INTRODUCTION
AUTOMATED PASSWORD RECOVERY
Welcome to a tutorial on how to create a forgotten Password recovery script in PHP and MySQL. Congratulations on building a user authentication system for your project, code ninja. But here comes the dreaded human part where people forget their passwords… Pretty sure that manually resetting passwords is not the best solution, and so, we shall walk through how to automate that process in this guide – In a step-by-step and secure manner, of course. Read on to find out!
I have included a zip file with all the source code at the end of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.
CONFESSION
AN HONEST DISCLOSURE
Quick, hide your wallets! I am an affiliate partner of Google, eBay, Adobe, Bluehost, Clickbank, and more. There are affiliate links and advertisements throughout this website. Whenever you buy things from the evil links that I recommend, I will make a commission. Nah. These are just things to keep the blog going, and allows me to give more good stuff to you guys - for free. So thank you if you decide to pick up my recommendations!
NAVIGATION
TABLE OF CONTENTS
Prelude | Step 1 | Step 2 |
Step 3 | Extra | Extra |
Closing |
PRELUDE
OVERVIEW & ASSUMPTIONS
Before we dive into the code part, here is an overview of the reset process, and some assumptions that I have made – So that you know what to expect from this guide.
ASSUMPTIONS
Everyone should already have a user system, or it will not make any sense to want a password recovery mechanism… So in this guide, we will purely touch on password reset only. If you do not have a user system yet, I will leave a link in the extras section below to another guide on how to create one.
Also, I shall assume that most of you guys are already code ninjas who are comfortable with PHP, MySQL, HTML, CSS, and Javascript – We will not go into the tiny boring details such as “how to import SQL files”.
PROJECT FOLDERS
If you have downloaded the zip file, here is a quick introduction to how the folders are being organized. If you want to follow along step-by-step, then start by creating the following folders.
-
ROOT
The project root folder, where we put the landing pages.-
lib
Where we store the config and library files. -
sql
Import these to create the necessary database tables. Delete afterward, not required for the project.
-
PROCESS OVERVIEW
Brace yourselves. This is going to be a very complicated process that involves 2 parts:
- The user requests for password recovery. Enters the email address into an HTML form, and a confirmation link will be sent.
- The user clicks on the confirmation link in the email. A random new password will be generated and sent to the user.
Done.
STEP 1
THE DATABASE
Now that we are done with the overview, let us start by building the foundations of the system – The database tables.
DUMMY USER TABLE
If you have not created a users table yet, here is a simple one that we shall use for this tutorial.
CREATE TABLE `users` (
`user_id` int(11) NOT NULL,
`user_email` varchar(255) NOT NULL,
`user_name` varchar(255) NOT NULL,
`user_password` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `users` (`user_id`, `user_email`, `user_name`, `user_password`) VALUES
(1, '[email protected]', 'John Doe', '123456');
ALTER TABLE `users`
ADD PRIMARY KEY (`user_id`),
ADD UNIQUE KEY `user_email` (`user_email`),
ADD KEY `user_name` (`user_name`);
ALTER TABLE `users`
MODIFY `user_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
Field | Description |
user_id | The user ID. Primary key, auto increment. |
user_email | The user’s email address. Unique. |
user_name | The user’s full name. |
user_password | The user’s password. Please take note – This is just a plain text field, do your own password encryption in PHP! |
PASSWORD RESET REQUEST TABLE
CREATE TABLE `password_reset` (
`user_id` int(11) NOT NULL,
`reset_hash` varchar(64) NOT NULL,
`reset_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `password_reset`
ADD PRIMARY KEY (`user_id`);
Field | Description |
user_id | Primary and foreign key. The user who made the reset request. |
reset_hash | A randomly generated security hash. Used to prevent unauthorized people from resetting the password. |
reset_time | A timestamp to prevent spammers from creating multiple reset requests in a short amount of time. |
STEP 2
THE LIBRARY FILES
Moving on, let us build the PHP library files that will deal with the password reset request.
CONFIG FILE
This one should be self-explanatory. Just remember to change the database and password reset settings to your own.
DATABASE LIBRARY
pdo = new PDO(
$str, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
return true;
}
// ERROR - DO SOMETHING HERE
// THROW ERROR MESSAGE OR SOMETHING
catch (Exception $ex) {
print_r($ex);
die();
}
}
function __destruct() {
// __destruct() : close connection when done
if ($this->stmt !== null) { $this->stmt = null; }
if ($this->pdo !== null) { $this->pdo = null; }
}
function createDB($db="", $user=DB_USER, $pass=DB_PASSWORD) {
// createDB() : create a new database
// PARAM $db : name of new database
// $user : user/owner of new database
// $password : password
$sql = "CREATE DATABASE `" . $db . "`;";
$sql .= "CREATE USER '" . $user . "'@'localhost' IDENTIFIED BY '" . $pass . "';";
$sql .= "GRANT ALL ON `" . $db . "`.* TO '" . $user . "'@'localhost';";
$sql .= "FLUSH PRIVILEGES;";
try {
$this->pdo->exec($sql);
} catch (Exception $ex) {
$this->error = $ex;
return false;
}
return true;
}
function createTable($name="", $fields) {
// createTable() : create a new table
// PARAM $name : name of table
// $fields : array of fields
$sql = "CREATE TABLE " . $name . " (";
foreach ($fields as $f) {
$sql .= $f . ",";
}
$sql = substr($sql, 0, -1) . ");";
try {
$this->pdo->exec($sql);
} catch (Exception $ex) {
$this->error = $ex;
return false;
}
return true;
}
function exec($sql, $data=null) {
// exec() : run insert, replace, update, delete query
// PARAM $sql : SQL query
// $data : array of data
try {
$this->stmt = $this->pdo->prepare($sql);
$this->stmt->execute($data);
$this->lastID = $this->pdo->lastInsertId();
} catch (Exception $ex) {
$this->error = $ex;
return false;
}
$this->stmt = null;
return true;
}
function fetchAll ($sql, $cond=null, $key=null, $value=null) {
// fetchAll() : perform select query (multiple rows expected)
// PARAM $sql : SQL query
// $cond : array of conditions
// $key : sort in this $key=>data order, optional
// $value : $key must be provided. If string provided, sort in $key=>$value order. If function provided, will be a custom sort.
$result = [];
try {
$this->stmt = $this->pdo->prepare($sql);
$this->stmt->execute($cond);
// Sort in given order
if (isset($key)) {
if (isset($value)) {
if (is_callable($value)) {
while ($row = $this->stmt->fetch(PDO::FETCH_NAMED)) {
$result[$row[$key]] = $value($row);
}
} else {
while ($row = $this->stmt->fetch(PDO::FETCH_NAMED)) {
$result[$row[$key]] = $row[$value];
}
}
} else {
while ($row = $this->stmt->fetch(PDO::FETCH_NAMED)) {
$result[$row[$key]] = $row;
}
}
}
// No key-value sort order
else {
$result = $this->stmt->fetchAll();
}
} catch (Exception $ex) {
$this->error = $ex;
return false;
}
// Return result
$this->stmt = null;
return count($result)==0 ? false : $result ;
}
function fetch ($sql, $cond=null, $sort=null) {
// fetch() : perform select query (single row expected)
// returns an array of column => value
// PARAM $sql : SQL query
// $cond : array of conditions
// $sort : custom sort function
$result = [];
try {
$this->stmt = $this->pdo->prepare($sql);
$this->stmt->execute($cond);
if (is_callable($sort)) {
while ($row = $this->stmt->fetch(PDO::FETCH_NAMED)) {
$result = $sort($row);
}
} else {
while ($row = $this->stmt->fetch(PDO::FETCH_NAMED)) {
$result = $row;
}
}
} catch (Exception $ex) {
$this->error = $ex;
return false;
}
// Return result
$this->stmt = null;
return count($result)==0 ? false : $result ;
}
function fetchCol ($sql, $cond=null) {
// fetchCol() : yet another version of fetch that returns a flat array
// I.E. Good for one column SELECT `col` FROM `table`
$this->stmt = $this->pdo->prepare($sql);
$this->stmt->execute($cond);
$result = $this->stmt->fetchAll(PDO::FETCH_COLUMN, 0);
return count($result)==0 ? false : $result;
}
function start () {
// start() : auto-commit off
$this->pdo->beginTransaction();
}
function end ($commit=1) {
// end() : commit or roll back?
if ($commit) { $this->pdo->commit(); }
else { $this->pdo->rollBack(); }
}
}
?>
Holy cow! This one looks like it is capable of causing permanent head damage (PHD). But keep calm and study closely – This is actually just a “standard” PDO database library that is used to do various SQL yoga.
Function | Description |
__construct | The constructor. Will automatically connect to the database when the object is created. |
__destruct | The destructor. Will automatically close the database connection when the object is destroyed. |
createDB | Create a new database. |
createTable | Create a new table. |
exec | Runs an insert, replace, update, or delete SQL query. |
fetchAll | Runs a select SQL query. Returns a numeric array of the results – Good for multiple row queries. |
fetch | Runs a select SQL query. Returns an associative array of the results – Good for single row queries. |
fetchCol | Runs a specific select SQL query on a single column only. Returns a numeric array of the results. |
USERS LIBRARY
fetch($sql, [$email]);
if ($user == false) {
$this->error = "$email is not registered";
return false;
}
// (2) Check for previous reset requests
// Fights spam, stops people from brute force reset request
// @TODO - Remove this if you don't need it
$sql = "SELECT * FROM `password_reset` WHERE `user_id`=?";
$request = $this->fetch($sql, [$user['user_id']]);
$now = strtotime("now");
if (is_array($request)) {
$expire = strtotime($request['reset_time']) + (PR_VALID * 60);
if ($now error = "Please try again later";
return false;
}
}
// (3) Create new reset request entry
$sql = "REPLACE INTO `password_reset` (`user_id`, `reset_hash`, `reset_time`) VALUES (?, ?, ?)";
$hash = md5($now . $user['user_id'] . PR_VALID); // Generates random hash - algo is up to you
$data = [$user['user_id'], $hash, date("Y-m-d H:i:s")];
if ($this->exec($sql, $data) == false) {
return false;
}
// (4) Email user reset link
// @TODO - Formulate a nice email
$subject = "Password Reset";
// Remember that the user ID and hash needs to be in the query string!
$message = "Click here to reset your password.";
if (@mail($user['user_email'], $subject, $message)) {
return true;
} else {
$this->error = "Error sending email";
return false;
}
}
function resetB ($id, $hash) {
// resetB() : Second part - Verify and reset password
// PARAM $id : user ID
// $hash : security hash
// (1) Verify given user ID and hash
$sql = "SELECT * FROM `password_reset` p LEFT JOIN `users` u USING (`user_id`) WHERE p.`user_id`=? AND p.`reset_hash`=?";
$request = $this->fetch($sql, [$id, $hash]);
if ($request == false) {
$this->error = "Invalid request";
return false;
}
// (2) Check if still valid
$now = strtotime("now");
$expire = strtotime($request['reset_time']) + (PR_VALID * 60);
if ($now >= $expire) {
$this->error = "Invalid request";
return false;
}
// (3) Remove reset request
$sql = "DELETE FROM `password_reset` WHERE `user_id`=?";
if (!$this->exec($sql, [$id])) {
return false;
}
// (4) Generate new random password
// @TODO - Change the password strength if you want
// @TODO - Password encryption
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-=+?";
$password = substr(str_shuffle($chars),0 ,8); // 8 characters
$sql = "UPDATE `users` SET `user_password`=? WHERE `user_id`=?";
if (!$this->exec($sql, [$password, $id])) {
return false;
}
// (5) Email
// @TODO - Formulate a nice email
$subject = "New Password";
$message = "Your new password is " . $password;
if (@mail($request['user_email'], $subject, $message)) {
return true;
} else {
$this->error = "Error sending email";
return false;
}
}
}
?>
Holy Bos Taurus! This one looks like another trouble script. But again, there are only 2 functions here that will deal with the password reset. If you take some time to walk through section-by-section, they are actually very straightforward.
Function | Description |
resetA | Will deal with the first part of the reset request. Validates the given email address, creates a reset request database entry, and sends the reset link to the user via email. |
resetB | Will deal with the second part of the reset request. Validates the given user ID and security hash. Generates a new random password and sends it to the user via email. |
STEP 3
HTML LANDING PAGES
Finally, all we need is to create the HTML landing pages.
RESET PASSWORD REQUEST PAGE
resetA($_POST['user_email']);
$message = $pass ? "OK - Check your email." : $libUser->error;
}
// (B) SHOW HTML ?>
Password Reset Request Demo
$message
Yep, this is but a simple page with 2 parts:
- The bottom part is nothing but a regular Joe HTML form to collect the user’s email address.
- When the user submits the form, the top part will use the libraries to handle the reset request (verify email, send reset link via email).
RESET CONFIRMATION PAGE
resetB($_GET['u'], $_GET['h']);
$message = $pass ? "OK - Check your email." : $libUser->error;
} else {
$message = "Invalid credentials";
}
// (B) SHOW HTML ?>
Password Reset Request Demo
$message";
} ?>
This is the page that users will land on after they click on the link in their email. Again, there are 2 parts to it:
- The top part will use the libraries to handle the reset request (verify id, hash, send the new password via email).
- The bottom half is nothing but HTML to show if the reset is successful or not.
EXTRA
USEFUL BITS
That’s all for this project, and here is a small section on some extras that may be useful to you.
USER LOGIN AND AUTHENTICATION
Have not created your own user system yet? Here’s how to do it:
3 Steps Simple PHP Login Page (With MySQL Database)
PASSWORD
Don’t leave the password in plain text! At least put a simple lock on it:
4 Ways to Encrypt, Decrypt and Verify Passwords in PHP
EXTRA
DOWNLOAD
Finally, here is the download link to the source code as promised.
QUICK START
Skipped the entire tutorial? Here are a few quick steps to set up the example:
- Download and unzip into your project folder.
- Create your dummy database and import the files in the
sql
folder. - Update the database and password settings in
lib/config.php
. - Access
1-request.php
and trace the code from there.
SOURCE CODE DOWNLOAD
Click here to download the source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.
CLOSING
WHAT’S NEXT?
Thank you for reading, and we have come to the end of this guide. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!
The post 3 Steps Forget Password Recovery Script With PHP appeared first on Code Boxx.