Skip to content

Instantly share code, notes, and snippets.

@code-boxx
Last active May 26, 2023 03:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save code-boxx/a5a0343a155fbbf20e420d6b8843a2ea to your computer and use it in GitHub Desktop.
Save code-boxx/a5a0343a155fbbf20e420d6b8843a2ea to your computer and use it in GitHub Desktop.
PHP MYSQL Forgotten Password Recovery

PHP MYSQL FORGOT PASSWORD RECOVERY

https://code-boxx.com/forgotten-password-recovery-php-mysql/

NOTES

  1. Create a test database and import 1-database.sql.
  2. Update the database settings in 2-lib.php to your own.
  3. 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.
  4. Launch 3a-forgot.php in the browser.

LICENSE

Copyright by Code Boxx

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

-- (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, 'jon@doe.com', 'Jon Doe', '123456');
-- (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`);
<?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; }
}
// (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();
}
// (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;
}
}
// (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;
}
}
}
// (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();
<!DOCTYPE html>
<html>
<head>
<title>Password Reset Demo</title>
<meta charset="utf-8">
<link rel="stylesheet" href="x-dummy.css">
</head>
<body>
<!-- (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
);
}
?>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Password Reset Demo</title>
<meta charset="utf-8">
<link rel="stylesheet" href="x-dummy.css">
</head>
<body>
<div class="note"><?php
require "2-lib.php";
echo $_FORGOT->reset($_GET["i"], $_GET["h"]) ? "New password sent to your email" : $_FORGOT->error
?></div>
</body>
</html>
/* (X) NOT IMPORTANT - COSMETICS */
* {
font-family: Arial, Helvetica, sans-serif;
box-sizing: border-box;
}
body {
width: 400px;
padding: 20px;
margin: 0 auto;
background: #f7f7f7;
}
form {
padding: 20px;
border: 1px solid #efefef;
background: #fff;
}
label, input {
display: block;
width: 100%;
}
label { margin-bottom: 10px; }
input { padding: 10px; }
input[type=email] { border: 1px solid #e3e3e3; }
input[type=submit] {
margin-top: 20px;
border: 0;
color: #fff;
background: #3f48b9;
cursor: pointer;
}
div.note {
margin-top: 10px;
padding: 10px;
border: 1px solid #ffc4c4;
background: #ffeaea;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment