1. Code
  2. PHP

Creating an Advanced Password Recovery Utility

Scroll to top

In my last tutorial, 'A Better Login System', a few people commented on how they would like to see a tutorial on password recovery, which is something you don't always see in user access tutorials. The tutorial I am bringing you today will deal with just that. Using mySQLi, we will learn to recover unencrypted and (one-way) encrypted passwords.

Introduction

Password recovery is always a useful feature to have on your site; however, many tutorials dealing with user authentication neglect this topic. In this tutorial, I will cover handling encrypted and unencrypted passwords, basic mySQLi functions, and we will also build in a temporary lockout if the user answers the security question incorrectly too many times. I hope that by the end of this tutorial, you will have a better understanding of recovering passwords, and the mySQLi interface.

For unencrypted passwords, we will create an email that will email the password to the user's registered email address.

When dealing with encrypted passwords, things are a little more complicated. When a password is encrypted with md5(), it can't be decrypted. Because of this, we will send the user an email with a link that will allow them to reset their password. To secure this process we will use a unique email key that will be checked when the user returns to the password page. Our passwords will be encrypted with a salt to improve security.

IMPORTANT NOTE: This tutorial uses the mysqli interface instead of mysql. Although the PHP documentation says that it should work, I was unable to use the mysqli classes successfully until I upgraded from 5.1.4 to 5.2.9. If you are getting errors about using an 'unsupported buffer', try the upgrade. You may also have to change your php.ini file to load the mysqli extension if you see errors about not finding the class mysqli.

Depending on whether your site uses encrypted or unencrypted user passwords, you should follow the directions a little differently. The directions as a whole are for encrypted passwords; but there are special notes here and there for dealing with the regular passwords.

Step 1: Database Tables

After creating a new database, we need to create three tables:

Database tablesDatabase tablesDatabase tables

The recoveryemails_enc table holds information about the emails that are sent out to allow users to reset passwords (such as the security key, the userID and expiration date). The users tables holds the user information. If you are following the encrypted example, use the users_enc table, otherwise use the table users.

If you already have a table of users, you will need to add security question and answer fields. The question field will hold an integer which equates to a security question in an array, and the answer field holds a varchar answer. The security question is used as verification before sending a password email. Here is the code you should run to create the tables (also available in sql.txt in the download):

1
2
CREATE TABLE IF NOT EXISTS `recoveryemails_enc` (
3
  `ID` bigint(20) unsigned zerofill NOT NULL auto_increment,
4
  `UserID` bigint(20) NOT NULL,
5
  `Key` varchar(32) NOT NULL,
6
  `expDate` datetime NOT NULL,
7
  PRIMARY KEY  (`ID`)
8
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
9
CREATE TABLE IF NOT EXISTS `users` (
10
  `ID` bigint(20) unsigned zerofill NOT NULL auto_increment,
11
  `Username` varchar(20) NOT NULL,
12
  `Email` varchar(255) NOT NULL,
13
  `Password` varchar(20) NOT NULL,
14
  `secQ` tinyint(4) NOT NULL,
15
  `secA` varchar(30) NOT NULL,
16
  PRIMARY KEY  (`ID`)
17
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=5 ;
18
INSERT INTO `users` (`ID`, `Username`, `Email`, `Password`, `secQ`, `secA`) VALUES (00000000000000000002, 'jDoe', 'jDoe@gmail.com', 'johnDoe2009', 0, 'Smith'),
19
(00000000000000000003, 'envato', 'webmaster@envato.com', 'envatouser', 1, 'Sydney'),
20
(00000000000000000004, 'sToaster', 'toast@yahoo.com', 'toastrules', 3, '2001');
21
CREATE TABLE IF NOT EXISTS `users_enc` (
22
  `ID` bigint(20) unsigned zerofill NOT NULL auto_increment,
23
  `Username` varchar(20) NOT NULL,
24
  `Password` char(32) NOT NULL,
25
  `Email` varchar(255) NOT NULL,
26
  `secQ` tinyint(4) NOT NULL default '0',
27
  `secA` varchar(32) NOT NULL,
28
  PRIMARY KEY  (`ID`)
29
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;
30
INSERT INTO `users_enc` (`ID`, `Username`, `Password`, `Email`, `secQ`, `secA`) VALUES (00000000000000000001, 'jDoe', 'fd2ba57673c57ac5a0650c38fe60b648', 'jDoe@gmail.com', 0, 'Smith'),
31
(00000000000000000002, 'envato', '1ecc663314777c8e3c2328027447f194', 'webmaster@envato.com', 1, 'Sydney'),
32
(00000000000000000003, 'sToaster', 'e05fd29cbca7ea9add48ba6dafc300e8', 'toast@yahoo.com', 3, '2001');

Step 2: Database Include

Database ConnectionDatabase ConnectionDatabase Connection

We need to create an include file so that we can connect to our database. For our database interaction, we will use mysqli, which provides an object-oriented approach to database interaction. Create a file called assets/php/database.php. We will add the following code to it (replace the variable values with the information appropriate for your hosting situation):

1
2
<?php
3
session_start();
4
ob_start();
5
$hasDB = false;
6
$server = 'localhost';
7
$user = 'user';
8
$pass = 'password';
9
$db = 'db';
10
$mySQL = new mysqli($server,$user,$pass,$db);
11
if ($mySQL->connect_error)
12
{
13
    die('Connect Error (' . $mySQL->connect_errno . ') '. $mySQL->connect_error);
14
}
15
16
?>

In the first line of the code, we call session_start(), we will not actually use the session variables but you will need a session as part of a user login system. Then, we call ob_start() to start the output buffer.

Lines 4-8 set up our variables to connect to the server. Then we create a new mysqli object by passing it our variables. For the remainder of our script, $mySQL will allow us to interact with the database as needed.

Mysqli handles error events slightly different, so we have to check the connect_error property after connecting, and print an error message if something went wrong. This goes the same for making queries; we must check the error property of our query or connection object to check for errors.

Step 3: Create the Forgot Password Page

Let's start off this step by creating the file forgotPass.php. Then, we'll add this code to it:

1
2
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
3
<html>
4
<head>
5
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
6
<title>Password Recovery</title>
7
<link href="assets/css/styles.css" rel="stylesheet" type="text/css">
8
</head>
9
<body>
10
<div id="header"></div>
11
<div id="page">
12
<!--PAGE CONTENT-->
13
</div>
14
</body>
15
</html>

This html gives us a nice base to build on and gives us this:

Page TamplatePage TamplatePage Tamplate

Then, add this code to the very top of the page, above the html we just entered:

1
2
<?php
3
include("assets/php/database.php"); 
4
include("assets/php/functions.php");
5
$show = 'emailForm'; //which form step to show by default

6
if ($_SESSION['lockout'] == true && (mktime() > $_SESSION['lastTime'] + 900))
7
{
8
	$_SESSION['lockout'] = false;
9
	$_SESSION['badCount'] = 0;
10
}
11
if (isset($_POST['subStep']) && !isset($_GET['a']) && $_SESSION['lockout'] != true)
12
{
13
	switch($_POST['subStep'])
14
	{
15
		case 1:
16
			//we just submitted an email or username for verification

17
			$result = checkUNEmail($_POST['uname'],$_POST['email']);
18
			if ($result['status'] == false )
19
			{
20
				$error = true;
21
				$show = 'userNotFound';
22
			} else {
23
				$error = false;
24
				$show = 'securityForm';
25
				$securityUser = $result['userID'];
26
			}
27
		break;
28
		case 2:
29
			//we just submitted the security question for verification

30
			if ($_POST['userID'] != "" && $_POST['answer'] != "")
31
			{
32
				$result = checkSecAnswer($_POST['userID'],$_POST['answer']);
33
				if ($result == true)
34
				{
35
					//answer was right

36
					$error = false;
37
					$show = 'successPage';
38
					$passwordMessage = sendPasswordEmail($_POST['userID']);
39
					$_SESSION['badCount'] = 0;
40
				} else {
41
					//answer was wrong

42
					$error = true;
43
					$show = 'securityForm';
44
					$securityUser = $_POST['userID'];
45
					$_SESSION['badCount']++;
46
				}
47
			} else {
48
				$error = true;
49
				$show = 'securityForm';
50
			}
51
		break;
52
		case 3:
53
			//we are submitting a new password (only for encrypted)

54
			if ($_POST['userID'] == '' || $_POST['key'] == '') header("location: login.php");
55
			if (strcmp($_POST['pw0'],$_POST['pw1']) != 0 || trim($_POST['pw0']) == '')
56
			{
57
				$error = true;
58
				$show = 'recoverForm';
59
			} else {
60
				$error = false;
61
				$show = 'recoverSuccess';
62
				updateUserPassword($_POST['userID'],$_POST['pw0'],$_POST['key']);
63
			}
64
		break;
65
	}
66
}

The first things we do are include the database file and a functions file that we will create shortly. Then we create a variable called $show that determines which interface element to display, by default we want to show the email input form.

After that, we check some session variables. If the session lockout variable is true, and more than 900 seconds (15 min) have elapsed since the lockout took place, we want to end the lockout, which lines 7 and 8 do for us.

Then we create an if block that checks to see if anything has been posted to the page. You can also see that we are checking to make sure that $_GET['a'] is not set, this variable will be set when the user clicks on the link in their recovery email, so if that is happening, we can skip the post check. Also in the logic block, we make sure that the lockout variable isn't set to true.

Once inside the if block, we use switch($_POST['subStep']) to check which stage of the form has been submitted. There are three stages that we will handle: Step 1 means that we just entered a username or email address that we want to reset the password for. To do that, we call the function checkUNEmail(), which we will write momentarily. This function returns an array that has a boolean value to determine if the user was found, and an integer of the userID if the user was found. Line 17 checks to see if the user check returned false (the user wasn't found), if so we set the error flag, and the $show variable so that we see the 'User Not Found' message. If the user was found, we should show the security question form, and set $securityUser so we know which user to load the question for.

In stage 2, we have submitted the security question. The first thing we do here is check to make sure that both a userID and an answer were returned. If either one was omitted, we display the security question form again. If both values were included, we call a function to check the answer against the database. This function returns a simple boolean. If the question was answered correctly, we will show the success page, send the reset email, and set the bad entry count to 0. If the question was answered incorrectly, we display the question page again, and also increment the bad answer counter.

The third stage happens after the user has received the password reset email, and has clicked on the link and submitted the password reset form. First, either userID or the security key are empty, we redirect to the homepage. Then we check to make sure that the two passwords match and were not empty. If they don't match, we show the password reset form again. If they do match, though, we show the success message, and call a function to reset the user's password.

NOTE: If you are using unencrypted passwords, you can leave out case 3 above. This is the method that saves a new password after it has been reset. Since we can email unencrypted passwords, they don't need to reset it.

Now we will add an else block to that same if block from above:

1
2
elseif (isset($_GET['a']) && $_GET['a'] == 'recover' && $_GET['email'] != "") {
3
	$show = 'invalidKey';
4
	$result = checkEmailKey($_GET['email'],urldecode(base64_decode($_GET['u'])));
5
	if ($result == false)
6
	{
7
		$error = true;
8
		$show = 'invalidKey';
9
	} elseif ($result['status'] == true) {
10
		$error = false;
11
		$show = 'recoverForm';
12
		$securityUser = $result['userID'];
13
	}
14
}
15
if ($_SESSION['badCount'] >= 3)
16
{
17
	$show = 'speedLimit';
18
	$_SESSION['lockout'] = true;
19
	$_SESSION['lastTime'] = '' ? mktime() : $_SESSION['lastTime'];
20
}
21
?>

This else block determines if $_GET['a'] is set, which means that the user has come to the page via the email reset link. If we've figured out they clicked on the link, we want to check if they email key is valid using checkEmailKey(). If the key is not found or not valid, it returns boolean false, and we display the invalid key message. If the key is valid, however, it returns an array that we can grab the userID out of (so we know which user's password to reset).

NOTE: If you are using unencrypted passwords, you can leave out the elseif block. This deals with displaying the password change form after clicking on the link in the email.

The second if block there checks to see if the user has answered the security question incorrectly more than 3 times. If they have, we set the lockout variable to true, and set the lockout time, which is the time the lockout went into effect. Notice that if the lockout time is already set, we will use that variable, but if not, we will generate a new time.

Setting a speedlimit is important for security. Mostly it will serve to deter people from just sitting there and trying to guess the answer to security questions. It can also help to prevent bots from submitting the form over and over. The main drawback of this method is that a user can just delete the PHPSESS_ID cookie, and then they can start over. If you want a very secure solution, you could add a 'lockout' flag to the users table, and when they have answered wrong 3 times, set that flag to true. Then you can write some code to lockout if they try to access the account.

Step 4: Function file

FunctionsFunctionsFunctions

Now let's create the file assets/php/functions.php to hold all of the functions we were referencing earlier. Now let's go through them a few at a time:

NOTE: In the sample files, the encrypted example uses the table 'users_enc', while the unencrypted example uses the table 'users' so the table name will change in your functions depending on which version you use.

1
2
<?php
3
define(PW_SALT,'(+3%_');
4
5
function checkUNEmail($uname,$email)
6
{
7
	global $mySQL;
8
	$error = array('status'=>false,'userID'=>0);
9
	if (isset($email) && trim($email) != '') {
10
		//email was entered

11
		if ($SQL = $mySQL->prepare("SELECT `ID` FROM `users_enc` WHERE `Email` = ? LIMIT 1"))
12
		{
13
			$SQL->bind_param('s',trim($email));
14
			$SQL->execute();
15
			$SQL->store_result();
16
			$numRows = $SQL->num_rows();
17
			$SQL->bind_result($userID);
18
			$SQL->fetch();
19
			$SQL->close();
20
			if ($numRows >= 1) return array('status'=>true,'userID'=>$userID);
21
		} else { return $error; }
22
	} elseif (isset($uname) && trim($uname) != '') {
23
		//username was entered

24
		if ($SQL = $mySQL->prepare("SELECT `ID` FROM `users_enc` WHERE Username = ? LIMIT 1"))
25
		{
26
			$SQL->bind_param('s',trim($uname));
27
			$SQL->execute();
28
			$SQL->store_result();
29
			$numRows = $SQL->num_rows();
30
			$SQL->bind_result($userID);
31
			$SQL->fetch();
32
			$SQL->close();
33
			if ($numRows >= 1) return array('status'=>true,'userID'=>$userID);
34
		} else { return $error; }
35
	} else {
36
		//nothing was entered;

37
		return $error;
38
	}
39
}

At the top of the file, we want to define PW_SALT, which is the salt we will use to encrypt passwords throughout. The first function takes the username and password values from the $_POST and sees if a matching user exists in the database. The first thing we need to do is make $mySQL global so we can access the database. We also create a generic error array that we will return if no user was found. Then we check to see if there is a value for the email. If there was, we build a mySQLi prepared statement. The great thing about using prepared statements is the security, especially when dealing with user input, the overall process is similar to using sprintf().

Notice that we call the ->prepare() method with our SQL query as the parameter, and use question marks where our variables would go. Also note that we don't put any form of quotes around the question marks (even for string).This is what allows us to parameterize a statement. mySQLi::prepare() will return true if the statement was created successfully. If it was created, we need to bind parameters to our query using mySQLi::bind_param(). This function accepts at least two arguments. The first is a string of letters that represent the datatypes of the data to be bound. The rest of the arguments are the variables that you want to be in each parameter. In our case, we are using 's' (for a string), and $email, since that variable has the email address. Note that the order is important, so the first question mark in your query corresponds to the first letter in the format string, and the first variable.

Then we need to call ->execute() and ->store_result(). These two methods run the prepared query and store it in memory until freed. Then we check for the number of rows returned to make sure that the user was found. ->bind_result() is very similar to ->bind_param() except in the opposite direction. It allows us to take a returned value from a result, and map it to a local variable. Variables are assigned based on their order in the result set. The local variable is not actually written until we call ->fetch(), after we call ->fetch(), the variable $userID will hold the variable from the database. Then we use ->close() on the query to free the result. Lastly, we will return an array that holds the boolean value for success and an integer for the userID.

1
2
function getSecurityQuestion($userID)
3
{
4
	global $mySQL;
5
	$questions = array();
6
	$questions[0] = "What is your mother's maiden name?";
7
	$questions[1] = "What city were you born in?";
8
	$questions[2] = "What is your favorite color?";
9
	$questions[3] = "What year did you graduate from High School?";
10
	$questions[4] = "What was the name of your first boyfriend/girlfriend?";
11
	$questions[5] = "What is your favorite model of car?";
12
	if ($SQL = $mySQL->prepare("SELECT `secQ` FROM `users_enc` WHERE `ID` = ? LIMIT 1"))
13
	{
14
		$SQL->bind_param('i',$userID);
15
		$SQL->execute();
16
		$SQL->store_result();
17
		$SQL->bind_result($secQ);
18
		$SQL->fetch();
19
		$SQL->close();
20
		return $questions[$secQ];
21
	} else {
22
		return false;
23
	}
24
}
25
26
function checkSecAnswer($userID,$answer)
27
{
28
	global $mySQL;
29
	if ($SQL = $mySQL->prepare("SELECT `Username` FROM `users_enc` WHERE `ID` = ? AND LOWER(`secA`) = ? LIMIT 1"))
30
	{
31
		$answer = strtolower($answer);
32
		$SQL->bind_param('is',$userID,$answer);
33
		$SQL->execute();
34
		$SQL->store_result();
35
		$numRows = $SQL->num_rows();
36
		$SQL->close();
37
		if ($numRows >= 1) { return true; }
38
	} else {
39
		return false;
40
	}
41
}

getSecurityQuestion() accepts the id of a user, and returns their security question as a string. Once again, we make $mySQL global. Then we create an array of the 6 different security questions possible. Using a method similar to the above, we see which security question the user has selected, then we return that index from the array.

checkSecAnswer() accepts a userID and a string answer and checks the database to see if it is correct. Note that we are converting the passed answer and the database answer to lowercase to increase chances of a match (optional for you). This is an excellent example of a multi parameter prepared statement. Notice the order of the arguments in the bind_param() method. This function will return true if it finds a record where the userID and the answer match what is passed. It will return false otherwise.

1
2
function sendPasswordEmail($userID)
3
{
4
	global $mySQL;
5
	if ($SQL = $mySQL->prepare("SELECT `Username`,`Email`,`Password` FROM `users_enc` WHERE `ID` = ? LIMIT 1"))
6
	{
7
		$SQL->bind_param('i',$userID);
8
		$SQL->execute();
9
		$SQL->store_result();
10
		$SQL->bind_result($uname,$email,$pword);
11
		$SQL->fetch();
12
		$SQL->close();
13
		$expFormat = mktime(date("H"), date("i"), date("s"), date("m")  , date("d")+3, date("Y"));
14
		$expDate = date("Y-m-d H:i:s",$expFormat);
15
		$key = md5($uname . '_' . $email . rand(0,10000) .$expDate . PW_SALT);
16
		if ($SQL = $mySQL->prepare("INSERT INTO `recoveryemails_enc` (`UserID`,`Key`,`expDate`) VALUES (?,?,?)"))
17
		{
18
			$SQL->bind_param('iss',$userID,$key,$expDate);
19
			$SQL->execute();
20
			$SQL->close();
21
			$passwordLink = "<a href=\"?a=recover&email=" . $key . "&u=" . urlencode(base64_encode($userID)) . "\">http://www.oursite.com/forgotPass.php?a=recover&email=" . $key . "&u=" . urlencode(base64_encode($userID)) . "</a>";
22
			$message = "Dear $uname,\r\n";
23
			$message .= "Please visit the following link to reset your password:\r\n";
24
			$message .= "-----------------------\r\n";
25
			$message .= "$passwordLink\r\n";
26
			$message .= "-----------------------\r\n";
27
			$message .= "Please be sure to copy the entire link into your browser. The link will expire after 3 days for security reasons.\r\n\r\n";
28
			$message .= "If you did not request this forgotten password email, no action is needed, your password will not be reset as long as the link above is not visited. However, you may want to log into your account and change your security password and answer, as someone may have guessed it.\r\n\r\n";
29
			$message .= "Thanks,\r\n";
30
			$message .= "-- Our site team";
31
			$headers .= "From: Our Site <webmaster@oursite.com> \n";
32
			$headers .= "To-Sender: \n";
33
			$headers .= "X-Mailer: PHP\n"; // mailer

34
			$headers .= "Reply-To: webmaster@oursite.com\n"; // Reply address

35
			$headers .= "Return-Path: webmaster@oursite.com\n"; //Return Path for errors

36
			$headers .= "Content-Type: text/html; charset=iso-8859-1"; //Enc-type

37
			$subject = "Your Lost Password";
38
			@mail($email,$subject,$message,$headers);
39
			return str_replace("\r\n","<br/ >",$message);
40
		}
41
	}
42
}

This function takes care of sending the email to reset a password. We start out by making a SQL query to get the username and email address of the passed user. After binding the parameters and the result, we close the query. For security reasons, we only want the link we generate to be good for 3 days. To do this, we create a new date that is 3 days in the future. Them, using several of the values that are passed in, the date, a random number and our salt, we generate an MD5 hash that will be our security key. Because of the every changing future date and random number, we should be assured a completely random key every time. Then we build a SQL query to insert it into the database. After executing the query, we make the link that will be sent in the email. We want to include 'a=recover' and our key, and userID which we base64_encode() and urlencode() to make it not readable. Once the link is generated, we make the rest of our email text and send it.

NOTE: If you are using the unencrypted example, you want to change your message text to instead print out the variable $pword instead of a link to reset the password.

NOTE: If you are developing this on a local server, it is unlikely that you have smtp installed, so you will be unable to send mail. This is why we are using the @mail() command (to prevent error messages). Also for that reason, this function returns the string of the message so it can be printed on the screen. Now for the last 3 function:

1
2
function checkEmailKey($key,$userID)
3
{
4
	global $mySQL;
5
	$curDate = date("Y-m-d H:i:s");
6
	if ($SQL = $mySQL->prepare("SELECT `UserID` FROM `recoveryemails_enc` WHERE `Key` = ? AND `UserID` = ? AND `expDate` >= ?"))
7
	{
8
		$SQL->bind_param('sis',$key,$userID,$curDate);
9
		$SQL->execute();
10
		$SQL->execute();
11
		$SQL->store_result();
12
		$numRows = $SQL->num_rows();
13
		$SQL->bind_result($userID);
14
		$SQL->fetch();
15
		$SQL->close();
16
		if ($numRows > 0 && $userID != '')
17
		{
18
			return array('status'=>true,'userID'=>$userID);
19
		}
20
	}
21
	return false;
22
}
23
24
function updateUserPassword($userID,$password,$key)
25
{
26
	global $mySQL;
27
	if (checkEmailKey($key,$userID) === false) return false;
28
	if ($SQL = $mySQL->prepare("UPDATE `users_enc` SET `Password` = ? WHERE `ID` = ?"))
29
	{
30
		$password = md5(trim($password) . PW_SALT);
31
		$SQL->bind_param('si',$password,$userID);
32
		$SQL->execute();
33
		$SQL->close();
34
		$SQL = $mySQL->prepare("DELETE FROM `recoveryemails_enc` WHERE `Key` = ?");
35
		$SQL->bind_param('s',$key);
36
		$SQL->execute();
37
	}
38
}
39
40
function getUserName($userID)
41
{
42
	global $mySQL;
43
	if ($SQL = $mySQL->prepare("SELECT `Username` FROM `users_enc` WHERE `ID` = ?"))
44
	{
45
		$SQL->bind_param('i',$userID);
46
		$SQL->execute();
47
		$SQL->store_result();
48
		$SQL->bind_result($uname);
49
		$SQL->fetch();
50
		$SQL->close();
51
	}
52
	return $uname;
53
}

The first function checks the email key that was passed. We make a date string to represent today's date (so we know if the key is expired). Then our SQL checks to see if any records exist for a key that matches the passed user, security key, and that the expiration date is after right now. If such a key exists, we return an array with the userID in it, or false otherwise.

The function updateUserPassword() takes care of changing the actual password in the database. First we verify the email key against the passed in user and key info to make sure things are safe. We generate a new password by combining the passed password and the previously defined salt, and then running md5() on that string. Then we perform our update. Once that is completed, we delete the record of the recovery key from the database so it can't be used again.

NOTE: The function updateUserPassword() is not needed in the unencrypted example, since we are not changing their password.

getUserName() is simply a utility function to get the username of the assed userID. It uses a basic SQL statement like the others.

Step 5: Complete the Forgot Password Page

Now that we have the functions we need built, we'll add some more to forgotPass.php. Add this code inside the div with the id "page":

1
<?php switch($show) {
2
	case 'emailForm': ?>
3
	<h2>Password Recovery</h2>
4
    <p>You can use this form to recover your password if you have forgotten it. Because your password is securely encrypted in our database, it is impossible actually recover your password, but we will email you a link that will enable you to reset it securely. Enter either your username or your email address below to get started.</p>
5
    <?php if ($error == true) { ?><span class="error">You must enter either a username or password to continue.</span><?php } ?>
6
    <form action="<?= $_SERVER['PHP_SELF']; ?>" method="post">
7
        <div class="fieldGroup"><label for="uname">Username</label><div class="field"><input type="text" name="uname" id="uname" value="" maxlength="20"></div></div>
8
        <div class="fieldGroup"><label>- OR -</label></div>
9
        <div class="fieldGroup"><label for="email">Email</label><div class="field"><input type="text" name="email" id="email" value="" maxlength="255"></div></div>
10
        <input type="hidden" name="subStep" value="1" />
11
        <div class="fieldGroup"><input type="submit" value="Submit" style="margin-left: 150px;" /></div>
12
        <div class="clear"></div>
13
    </form>
14
    <?php break; case 'securityForm': ?>
15
    <h2>Password Recovery</h2>
16
    <p>Please answer the security question below:</p>
17
    <?php if ($error == true) { ?><span class="error">You must answer the security question correctly to receive your lost password.</span><?php } ?>
18
    <form action="<?= $_SERVER['PHP_SELF']; ?>" method="post">
19
        <div class="fieldGroup"><label>Question</label><div class="field"><?= getSecurityQuestion($securityUser); ?></div></div>
20
        <div class="fieldGroup"><label for="answer">Answer</label><div class="field"><input type="text" name="answer" id="answer" value="" maxlength="255"></div></div>
21
        <input type="hidden" name="subStep" value="2" />
22
        <input type="hidden" name="userID" value="<?= $securityUser; ?>" />
23
        <div class="fieldGroup"><input type="submit" value="Submit" style="margin-left: 150px;" /></div>
24
        <div class="clear"></div>
25
    </form>
26
27
	 <?php break; case 'userNotFound': ?><br>    <h2>Password Recovery</h2><br>    <p>The username or email you entered was not found in our database.<br /><br /><a href="?">Click here</a> to try again.</p><br>    <?php break; case 'successPage': ?><br>    <h2>Password Recovery</h2><br>    <p>An email has been sent to you with instructions on how to reset your password. <strong>(Mail will not send unless you have an smtp server running locally.)</strong><br /><br /><a href="login.php">Return</a> to the login page. </p><br>    <p>This is the message that would appear in the email:</p><br>    <div class="message"><?= $passwordMessage;?></div><br>    <?php break;

This code will deal with actually displaying all of the interface elements. We start off by examining the $show variable from earlier with switch(). As you may remember from earlier, there are many different values that $show can have, so we must create a case for each one. The first case, "emailForm", is where the user will enter either a username or email that they want to recover the password for. Basically we need two text fields, and a hidden input so we know which step we have submitted. There is also an if ($error == true) block that will display an error message if the $error flag is true.

Email FormEmail FormEmail Form

The second case is "securityForm". This is where the security question will be displayed. You can see that we also have an error message, and we use getSecurityQuestion() to print the security question. Then we have an input for the answer, as well as the step, and the userID.

Security QuestionSecurity QuestionSecurity Question

The message for "userNotFound" is simply a text message that tells the user that the record wasn't found.

"SuccessPage" is shown when the security question has been answered correctly and the email has been sent. Notice that we have printed out the email message at the bottom of this prompt, you would not do this in a production environment, because then you give the person instant access to the security code meant only for the registered user.

EmailEmailEmail

Let's continue with this code:

1
2
case 'recoverForm': ?>
3
    <h2>Password Recovery</h2>
4
    <p>Welcome back, <?= getUserName($securityUser=='' ? $_POST['userID'] : $securityUser); ?>.</p>
5
    <p>In the fields below, enter your new password.</p>
6
    <?php if ($error == true) { ?><span class="error">The new passwords must match and must not be empty.</span><?php } ?>
7
    <form action="<?= $_SERVER['PHP_SELF']; ?>" method="post">
8
        <div class="fieldGroup"><label for="pw0">New Password</label><div class="field"><input type="password" class="input" name="pw0" id="pw0" value="" maxlength="20"></div></div>
9
        <div class="fieldGroup"><label for="pw1">Confirm Password</label><div class="field"><input type="password" class="input" name="pw1" id="pw1" value="" maxlength="20"></div></div>
10
        <input type="hidden" name="subStep" value="3" />
11
        <input type="hidden" name="userID" value="<?= $securityUser=='' ? $_POST['userID'] : $securityUser; ?>" />
12
        <input type="hidden" name="key" value="<?= $_GET['email']=='' ? $_POST['key'] : $_GET['email']; ?>" />
13
        <div class="fieldGroup"><input type="submit" value="Submit" style="margin-left: 150px;" /></div>
14
        <div class="clear"></div>
15
    </form>
16
    <?php break; case 'invalidKey': ?>
17
    <h2>Invalid Key</h2>
18
    <p>The key that you entered was invalid. Either you did not copy the entire key from the email, you are trying to use the key after it has expired (3 days after request), or you have already used the key in which case it is deactivated.<br /><br /><a href="login.php">Return</a> to the login page. </p>
19
    <?php break; case 'recoverSuccess': ?>
20
    <h2>Password Reset</h2>
21
    <p>Congratulations! your password has been reset successfully.</p><br /><br /><a href="login.php">Return</a> to the login page. </p>
22
    <?php break; case 'speedLimit': ?>
23
    <h2>Warning</h2>
24
    <p>You have answered the security question wrong too many times. You will be locked out for 15 minutes, after which you can try again.</p><br /><br /><a href="login.php">Return</a> to the login page. </p>
25
    <?php break; }
26
	ob_flush();
27
	$mySQL->close();
28
?>

Continuing the switch statement, we have "recoverForm", which displays the form to change the password. We print out the user's name, and an error message if needed. Then we have input fields for a password and confirmation password, the submission step, the userID and the security key from the email. For the key and userID, e are using the special if() syntax to make sure that we are getting the variable from the right place. If the querystring variable is null, we will look in the post variables to make sure that we always have a value here.

Change PasswordChange PasswordChange Password

Then, "invalidKey" means that the key provided did not exist or was expired. The "speedLimit" message is displayed if the user has been locked out because of too many incorrect answers. Finally, we call ob_flush() to send the page to the browser, and $mySQL->close() to disconnect from the database.

NOTE: "recoverForm" and "invalidKey" messages from above are not needed for unencrypted passwords.

Conclusion

In this article, we've looked at adding an extremely useful feature for any membership site. Whether you store your passwords as plain text or encrypted, this tutorial should have helped you add a password recovery function. We looked at the logic behind encrypting passwords, sending emails to recover the passwords, and some security concerns when doing so.

Additionally, we looked at using mysqli to improve our interaction with the database. While this tutorial didn't go that in depth with mysqli, it should leave you a little better off than you were before.

I hope you can use the techniques discussed here to add this extremely useful feature to your next user membership site.

Note on the Download: In the download files, I have implemented both encrypted and unencrypted versions in two different folders.


Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.