Welcome to a quick tutorial on how to create a forgotten password recovery script with PHP and MySQL. Having trouble with a system that requires manual password reset?
An automated password recovery system generally involves 3 steps:
- The user accesses a “forgot password” page, enters the email, and makes a password reset request.
- The system generates a random hash and sends a confirmation link to the user’s email.
- Lastly, the user clicks on the confirmation link. The system verifies the hash and sends a new password to the user.
That covers the overview of the process, let us walk through an actual example in this guide – Read on!
TABLE OF CONTENTS
DOWNLOAD & NOTES
Here is the download link to the example code, so you don’t have to copy-paste everything.
EXAMPLE CODE DOWNLOAD
Just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.
SORRY FOR THE ADS...
But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.
Buy Me A Coffee Code Boxx eBooks
PHP PASSWORD RECOVERY
All right, let us now get into the details of building a forgotten password recovery system in PHP and MYSQL.
PART 1) THE DATABASE
1A) USERS TABLE
-- (A) USERS
CREATE TABLE `users` (
`user_id` bigint(20) NOT NULL,
`user_email` varchar(255) NOT NULL,
`user_name` varchar(255) NOT NULL,
`user_password` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
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` bigint(20) NOT NULL AUTO_INCREMENT;
INSERT INTO `users` (`user_id`, `user_email`, `user_name`, `user_password`) VALUES
(1, 'john@doe.com', 'John Doe', '123456');
Just in case you don’t already have an existing user database, here is a dummy that we will use in this example.
user_id
Primary key, auto-increment.user_email
User’s email, unique.user_name
User’s full name.user_password
User’s password. Take note, not encrypted. I will leave links below for you to follow up with.
1B) PASSWORD RESET TABLE
-- (B) PASSWORD RESET
CREATE TABLE `password_reset` (
`user_id` bigint(20) NOT NULL,
`reset_hash` varchar(64) NOT NULL,
`reset_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `password_reset`
ADD PRIMARY KEY (`user_id`);
user_id
– The user who made the “forgot password” request.reset_hash
– Random hash that is generated upon making the reset request. Will be sent to the user’s email for verification; If the hash in the reset link is the same as in the database, we can confirm it is a valid request.reset_time
– Time when the request is made, to prevent spam.
PART 2) PASSWORD RESET LIBRARY
2A) LIBRARY INITIALIZE
class Forgot {
// (A) PROPERTIES
private $valid = 900; // 15 mins = 900 secs
private $plen = 8; // random password length
private $pdo = null; // pdo object
private $stmt = null; // sql statement
public $error = ""; // errors, if any
// (B) CONSTRUCTOR - CONNECT TO DATABASE
function __construct () {
$this->pdo = new PDO(
"mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHARSET,
DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
}
// (C) DESTRUCTOR - CLOSE DATABASE CONNECTION
function __destruct () {
if ($this->stmt!==null) { $this->stmt = null; }
if ($this->pdo!==null) { $this->pdo = null; }
}
// ...
}
// (J) DATABASE SETTINGS - @CHANGE TO YOUR OWN
define("DB_HOST", "localhost");
define("DB_NAME", "test");
define("DB_CHARSET", "utf8mb4");
define("DB_USER", "root");
define("DB_PASSWORD", "");
// (K) NEW PASSWORD RECOVER OBJECT
$_FORGOT = new Forgot();
The library can be intimidating to some beginners, so let’s go through it in sections.
- (A) Some class properties, the only 2 that “matters” here are –
$valid
E.G. If the user makes a password reset request at 10 AM, it must be completed within 15 minutes; A new request can only be made after 10.15 AM.$plen
New password length, defaults to 8 characters.
- (B, C, K) When
$_FORGOT = new Forgot()
is created, the constructor connects to the database automatically. The destructor closes the connection. - (J) Remember to change the database settings to your own.
2B) HELPER & GET FUNCTIONS
// (D) HELPER - EXECUTE SQL QUERY
function query ($sql, $data=null) : void {
$this->stmt = $this->pdo->prepare($sql);
$this->stmt->execute($data);
}
// (E) HELPER - SEND EMAIL
function mail ($to, $subject, $body) {
$head = implode("\r\n", ["MIME-Version: 1.0", "Content-type: text/html; charset=utf-8"]);
return mail($to, $subject, $body, $head);
}
// (F) GET USER BY ID OR EMAIL
function getUser ($id) {
$this->query("SELECT * FROM `users` WHERE `user_".(is_numeric($id)?"id":"email")."`=?", [$id]);
return $this->stmt->fetch();
}
// (G) GET PASSWORD RESET REQUEST
function getReq ($id) {
$this->query("SELECT * FROM `password_reset` WHERE `user_id`=?", [$id]);
return $this->stmt->fetch();
}
Next, we have a whole bunch of “short functions”. Should be pretty self-explanatory.
query()
Helper function to run an SQL query.mail()
Helper function to send an email.getUser()
Get a user by ID or email.getReq()
Get password reset request by user ID.
2C) RESET REQUEST
// (H) PASSWORD RESET REQUEST
function request ($email) {
// (H1) ALREADY SIGNED IN - @CHANGE THIS TO YOUR OWN
// ASSUMING YOU USE $_SESSION["USER"]
if (isset($_SESSION["user"])) {
$this->error = "You are already signed in.";
return false;
}
// (H2) CHECK IF VALID USER
$user = $this->getUser($email);
if (!is_array($user)) {
$this->error = "$email is not registered.";
return false;
}
// (H3) CHECK PREVIOUS REQUEST (PREVENT SPAM)
$req = $this->getReq($user["user_id"]);
if (is_array($req)) {
$expire = strtotime($req["reset_time"]) + $this->valid;
$now = strtotime("now");
$left = $now - $expire;
if ($left <0) { $this->error = "Please wait another ".abs($left)." seconds.";
return false;
}
}
// (H4) CHECKS OK - CREATE NEW RESET REQUEST
$now = strtotime("now");
$hash = md5($user["user_email"] . $now); // random hash
$this->query(
"REPLACE INTO `password_reset` (`user_id`, `reset_hash`, `reset_time`) VALUES (?,?,?)",
[$user["user_id"], $hash, date("Y-m-d H:i:s")]
);
// (H5) SEND EMAIL TO USER - @CHANGE EMAIL TEMPLATE
$mail = "<html><body><a href='http://localhost/3b-reset.php?i={$user["user_id"]}&h={$hash}'>Click to reset</a></body></html>";
if ($this->mail($user["user_email"], "Password Reset", $mail)) { return true; }
else {
$this->error = "Error sending mail";
return false;
}
}
This is “step 2” as described in the introduction. Not going to explain line by line, but a quick walkthrough:
- (H1 to H3) Basically – A whole bunch of checks to see if the user is already signed in, is a valid email, or already has an existing request.
- (H1) I assume that the default user login is using
$_SESSION["user"]
. Change this accordingly to fit your system, will leave links below if you don’t have an existing login system. - (H4 & H5) Generate a random hash (random string), and send the reset link to the user’s email. Change the email template to your own.
2D) RESET VERIFICATION
// (I) PROCESS PASSWORD RESET
function reset ($id, $hash) {
// (I1) ALREADY SIGNED IN - @CHANGE THIS TO YOUR OWN
// ASSUMING YOU USE $_SESSION["USER"]
if (isset($_SESSION["user"])) {
$this->error = "You are already signed in.";
return false;
}
// (I2) CHECK REQUEST
$req = $this->getReq($id);
$pass = is_array($req);
// (I3) CHECK EXPIRE
if ($pass) {
$expire = strtotime($req["reset_time"]) + $this->valid;
$now = strtotime("now");
$pass = $now <= $expire;
}
// (I4) CHECK HASH
if ($pass) { $pass = $hash==$req["reset_hash"]; }
// (I5) GET USER
if ($pass) {
$user = $this->getUser($id);
$pass = is_array($user);
}
// (I6) CHECK FAIL - INVALID REQUEST
if (!$pass) {
$this->error = "Invalid request.";
return false;
}
// (I7) UPDATE USER PASSWORD - @CHANGE ENCRYPT PASSWORD
$this->pdo->beginTransaction();
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-=+?";
$password = substr(str_shuffle($chars), 0, $this->plen);
$this->query("UPDATE `users` SET `user_password`=? WHERE `user_id`=?", [$password, $id]);
// (I8) REMOVE REQUEST
$this->query("DELETE FROM `password_reset` WHERE `user_id`=?", [$id]);
// (I9) EMAIL TO USER - @CHANGE EMAIL TEMPLATE
$mail = "<html><body>$password</body></html>";
if ($this->mail($user["user_email"], "Password Reset", $mail)) {
$this->pdo->commit();
return true;
} else {
$this->error = "Error sending mail";
$this->pdo->rollBack();
return false;
}
}
Lastly, this is “step 3” of the reset process. Simply verify the random hash, create a new password, and send it to the user’s email. Once again:
- (I1) Change the login check to your own.
- (I7) Encrypt your password.
- (I9) Change the email template.
PART 3) FORGOT PASSWORD PAGES
3A) RESET REQUEST
<!-- (A) PASSWORD RESET FORM -->
<form method="post" target="_self">
<label>Email</label>
<input type="email" name="email" required value="jon@doe.com">
<input type="submit" value="Reset Password">
</form>
<!-- (B) PROCESS PASSWORD RESET REQUEST -->
<?php
if (isset($_POST["email"])) {
require "2-lib.php";
printf("<div class='note'>%s</div>",
$_FORGOT->request($_POST["email"]) ? "Click on the link in your email" : $_FORGOT->error
);
}
?>
- The “reset password” form, with only 1 field – email.
- When the form is submitted, we use
$_FORGOT->request()
to process it.
3B) RESET CONFIRMATION PAGE
<div class="note"><?php
require "2-lib.php";
echo $_FORGOT->reset($_GET["i"], $_GET["h"]) ? "New password sent to your email" : $_FORGOT->error
?></div>
Just using $_FORGOT->reset()
to complete the password reset.
EXTRAS
That’s all for this project, and here is a small section on some extras that may be useful to you.
BAREBONES ONLY – DO YOUR HOMEWORK
Before the grandmaster code ninja trolls start to spew acid – This is a barebones example… There are a lot of things that need and can be done better.
- Cosmetics, of course.
- Security – Add a CAPTCHA to further prevent spam.
- Password encryption.
- Enforce a temporary password, force the user to change it upon login.
So yep, I will just leave more links below that might further help you.
LINKS & REFERENCES
- Simple User Login System With PHP MySQL – Code Boxx
- PHP Password Encryption & Decryption – Code Boxx
- Fix “Email Not Sending” In PHP – Code Boxx
- Google reCAPTCHA
THE END
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!
hye there need a urgent help with codes i changed my emai but it still showing failed to send emai plzz help me as soon as possible thanku
Sorry, I cannot offer free troubleshooting for your personal server and project.
https://code-boxx.com/fix-php-mail-not-working/
https://code-boxx.com/faq/#help “Help and requests on a deeper level”
https://code-boxx.com/faq/#notwork
I used your code and it worked well, after I made it fit my page. It is still a work in process, =LINK REMOVED=
It is the “forgot your password section”
I was hoping to also use it to make a user – reset – their password , such that they can choose a password, but I can’t seem to allow user input into 2C-reset. Rather than a random password. Any thoughts?
Sorry, I cannot offer free project consultations. Ask around in other websites/groups/forums, hire a freelancer if you are stuck.
https://code-boxx.com/websites-get-help-programming/
https://code-boxx.com/faq/#notwork
https://code-boxx.com/faq/#help
I was able to follow and use your code for the random new password. I am also trying to allow a user to enter in the password of their choosing. But can’t modify your code to do that. Do you have any thoughts?
Update 2c-reset.php, here is a possible solution:
1) Put the user ID and hash check (A to C) into a
function check()
.2) Run
check()
, then show a change password form instead.3) When the form is submitted, run
check()
again, update the password in the database.But otherwise, I cannot help you with your personal project any further. Good luck.
https://code-boxx.com/faq/#help
Hello,
Thank you for your amazing tutorials and code, it is helping me alot in learning PHP.
I’m trying to join this code with your other tutorial’s, by integrating the various functions into one file.
I see that the code is very similar, but how can i integrate both codes to avoid redundancy?
I have successfully integrated the common code from your user registration and user login tutorials.
Build your own set of libraries, you decide how. Don’t want to restrict the way you structure your future projects, read examples from all over the Internet:
https://www.google.com/search?q=php+database+class
https://www.tutorialspoint.com/php/php_object_oriented.htm
https://code-boxx.com/simple-php-mvc-example/
Or study my existing projects if you want. Download the core and user module. See lib/Core.php and how the modules use the core database functions.
https://code-boxx.com/core-boxx-php-rapid-development-framework/
I like your Tut and it is working but when I receive email password reset link it is in this text format.
Can you please show how to set it to be html read that link is in the message ?
Thank you
Updated – Change email settings in 2b-forgot.php B4 to your own.