Forgotten Password Recovery In PHP MySQL (Free Download)

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:

  1. The user accesses a “forgot password” page, enters the email, and makes a password reset request.
  2. The system generates a random hash and sends a confirmation link to the user’s email.
  3. 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!

ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

 

TABLE OF CONTENTS

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example code as promised.

 

QUICK NOTES

  • Create a test database and import 1-database.sql.
  • Update the database settings in 2-lib.php to your own.
  • You may also want to do a search for @CHANGE in 2-lib.php and update the following sections.
    • Email templates.
    • User session/login checks.
    • Password encryption.
  • Launch 3a-forgot.php in the browser.
If you spot a bug, feel free to comment below. I try to answer short questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

SCREENSHOT

 

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

 

 

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

1-database.sql
-- (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

1-database.sql
-- (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

2-lib.php
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", "utf8");
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

2-lib.php
// (D) HELPER - EXECUTE SQL QUERY
function query ($sql, $data=null) {
  $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

2-lib.php
// (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

2-lib.php
// (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

3a-forgot.php
<!-- (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
  );
}
?>
  1. The “reset password” form, with only 1 field – email.
  2. When the form is submitted, we use $_FORGOT->request() to process it.

 

3B) RESET CONFIRMATION PAGE

3b-reset.php
<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.

 

EXTRA BITS & LINKS

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

 

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!

10 thoughts on “Forgotten Password Recovery In PHP MySQL (Free Download)”

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

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

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

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

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

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

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

Leave a Comment

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