One of the most important features of any secure website is the ability for users to reset their passwords, especially when they forget them. In this tutorial, we’ll walk through the steps of building a password reset system using PHP and MySQL. The system will allow users to request a password reset and choose a new password through an email link.
Overview of the Process
The process for implementing a password reset system can be broken down into three main steps. For clarity, we will discuss each step with respect to the forms and actions that users will go through:
- Login Form:
This is the main form where users enter their username and password to log in. If they’ve forgotten their password, there will be a “Forgot your password?” link, which redirects them to the second step of the process. - Email Form:
After clicking the “Forgot your password?” link, the user is prompted to enter their email address. If the email exists in the database, a unique token is generated and stored in apassword_resets
table along with the email. The user will receive an email with a link containing the token. This link will redirect them back to the website to reset their password. - New Password Form:
The user clicks on the link in the email, which contains the token. They are then redirected to a form where they can enter a new password. The system checks the token against thepassword_resets
table to verify the request and update the user’s password.
Database Structure
To get started, we need to create two tables in a MySQL database named password_recovery
:
Users Table (users
):
Field | Type | Specs |
---|---|---|
id | INT(11) | |
username | VARCHAR(255) | |
VARCHAR(255) | UNIQUE | |
password | VARCHAR(255) |
Password Resets Table (password_resets
):
Field | Type | Specs |
---|---|---|
id | INT(11) | |
VARCHAR(255) | ||
token | VARCHAR(255) | UNIQUE |
Project Setup
Create a project folder named password-recovery
in your server’s root directory (like htdocs
or www
). Inside this folder, create the following files:
login.php
– Login form pageenter_email.php
– Page for entering email to reset passwordnew_pass.php
– Page to enter a new passwordpending.php
– A page notifying the user to check their emailapp_logic.php
– Contains the PHP logic for handling user actionsmessages.php
– Displays form validation messagesmain.css
– Stylesheet for the forms
Step-by-Step Code Implementation
1. Login Form (login.php
)
This form allows users to log in or request a password reset.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <?php include('app_logic.php'); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Password Reset PHP</title> <link rel="stylesheet" href="main.css"> </head> <body> <form class="login-form" action="login.php" method="post"> <h2 class="form-title">Login</h2> <?php include('messages.php'); ?> <div class="form-group"> <label>Username or Email</label> <input type="text" value="<?php echo $user_id; ?>" name="user_id"> </div> <div class="form-group"> <label>Password</label> <input type="password" name="password"> </div> <div class="form-group"> <button type="submit" name="login_user" class="login-btn">Login</button> </div> <p><a href="enter_email.php">Forgot your password?</a></p> </form> </body> </html> |
2. Email Form (enter_email.php
)
This form allows users to enter their email address to receive the reset password link.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php include('app_logic.php'); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Password Reset PHP</title> <link rel="stylesheet" href="main.css"> </head> <body> <form class="login-form" action="enter_email.php" method="post"> <h2 class="form-title">Reset Password</h2> <?php include('messages.php'); ?> <div class="form-group"> <label>Your email address</label> <input type="email" name="email"> </div> <div class="form-group"> <button type="submit" name="reset-password" class="login-btn">Submit</button> </div> </form> </body> </html> |
3. New Password Form (new_pass.php
)
This form allows users to set a new password once they have verified their identity using the reset link.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Password Reset PHP</title> <link rel="stylesheet" href="main.css"> </head> <body> <form class="login-form" action="new_password.php" method="post"> <h2 class="form-title">New Password</h2> <?php include('messages.php'); ?> <div class="form-group"> <label>New Password</label> <input type="password" name="new_pass"> </div> <div class="form-group"> <label>Confirm New Password</label> <input type="password" name="new_pass_c"> </div> <div class="form-group"> <button type="submit" name="new_password" class="login-btn">Submit</button> </div> </form> </body> </html> |
CSS Styling (main.css
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | body { background: #3b5998; font-size: 1.1em; font-family: sans-serif; } a { text-decoration: none; } form { width: 25%; margin: 70px auto; background: white; padding: 10px; border-radius: 3px; } h2.form-title { text-align: center; } input { display: block; box-sizing: border-box; width: 100%; padding: 8px; } form .form-group { margin: 10px auto; } form button { width: 100%; border: none; color: white; background: #3b5998; padding: 15px; border-radius: 5px; } .msg { margin: 5px auto; border-radius: 5px; border: 1px solid red; background: pink; text-align: left; color: brown; padding: 10px; } |
PHP Logic (app_logic.php
)
The core logic of handling user actions, database interactions, and sending emails:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | <?php session_start(); $errors = []; $user_id = ""; // Connect to database $db = mysqli_connect('localhost', 'root', '', 'password-reset-php'); // LOG USER IN if (isset($_POST['login_user'])) { $user_id = mysqli_real_escape_string($db, $_POST['user_id']); $password = mysqli_real_escape_string($db, $_POST['password']); if (empty($user_id)) array_push($errors, "Username or Email is required"); if (empty($password)) array_push($errors, "Password is required"); if (count($errors) == 0) { $password = md5($password); $sql = "SELECT * FROM users WHERE username='$user_id' OR email='$user_id' AND password='$password'"; $results = mysqli_query($db, $sql); if (mysqli_num_rows($results) == 1) { $_SESSION['username'] = $user_id; $_SESSION['success'] = "You are now logged in"; header('location: index.php'); }else { array_push($errors, "Wrong credentials"); } } } // Reset password logic if (isset($_POST['reset-password'])) { $email = mysqli_real_escape_string($db, $_POST['email']); $query = "SELECT email FROM users WHERE email='$email'"; $results = mysqli_query($db, $query); if (empty($email)) { array_push($errors, "Your email is required"); } else if (mysqli_num_rows($results) <= 0) { array_push($errors, "Sorry, no user exists with that email"); } $token = bin2hex(random_bytes(50)); if (count($errors) == 0) { $sql = "INSERT INTO password_resets(email, token) VALUES ('$email', '$token')"; mysqli_query($db, $sql); // Send email with reset link $to = $email; $subject = "Reset your password on examplesite.com"; $msg = "Hi, click on this <a href='new_pass.php?token=$token'>link</a> to reset your password"; $msg = wordwrap($msg, 70); $headers = "From: info@examplesite.com"; mail($to, $subject, $msg, $headers); header('location: pending.php'); } } // New password logic if (isset($_POST['new_password'])) { $new_pass = mysqli_real_escape_string($db, $_POST['new_pass']); $new_pass_c = mysqli_real_escape_string($db, $_POST['new_pass_c']); $token = $_GET['token']; if (empty($new_pass)) { array_push($errors, "Password is required"); } else if ($new_pass != $new_pass_c) { array_push($errors, "The two passwords do not match"); } if (count($errors) == 0) { $new_pass = md5($new_pass); $sql = "UPDATE users JOIN password_resets ON users.email=password_resets.email SET users.password='$new_pass' WHERE password_resets.token='$token'"; mysqli_query($db, $sql); header('location: login.php'); } } ?> |
This guide provides the basic structure and logic for creating a password recovery system using PHP. You can further enhance the system with better error handling, security features like email validation, and user interface improvements.
Let me know if you’d like to see more about any particular section!
Final Enhancements
Now that we have the basic structure for the password reset system, let’s add a few improvements for security and user experience:
- Improve Security for Tokens:
- Token Expiry: To prevent misuse, we should set an expiration date for the reset token (e.g., 1 hour). After the token expires, it will no longer be valid for resetting the password.
- Hash the Token: Instead of storing the plain token in the database, you should hash it to add an extra layer of security.
- Validate Token on Password Reset Form:
- Before allowing the user to reset the password, check if the token is valid and not expired.
- Show Success or Error Messages:
- After submitting the new password, show a confirmation message to the user that the password has been updated successfully.
- Redirect After Email Sent:
- After sending the reset email, make sure users are redirected to a page that confirms the email was sent successfully (i.e., “Check your email to reset your password”).
- Password Strength Validation:
- Ensure that users choose a strong password by validating the password strength before submitting the form.
Adding Token Expiry and Hashing
We will now modify the password_resets
table to include an expiry date for the reset token. This change will ensure the token is only valid for a limited period.
1. Modify the Database Schema
1 2 3 4 5 6 7 8 | CREATE TABLE password_resets ( id INT(11) AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) NOT NULL, token VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); |
The created_at
column will store the timestamp when the token was created. The token will expire after a set duration, such as 1 hour.
2. Update app_logic.php
to Hash the Token and Add Expiry Check
Let’s update the PHP logic to hash the token and validate its expiry.
Generate the Token with Hashing:
1 2 3 4 5 | // Token generation logic $token = bin2hex(random_bytes(50)); $hashed_token = password_hash($token, PASSWORD_DEFAULT); |
Store the Token with the Expiry:
1 2 3 4 | $sql = "INSERT INTO password_resets(email, token, created_at) VALUES ('$email', '$hashed_token', NOW())"; mysqli_query($db, $sql); |
Validate the Token and Expiry when Resetting the Password:
When the user clicks on the reset link, we need to validate that the token exists, is not expired, and is correctly hashed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // Validate the token $token = $_GET['token']; $query = "SELECT * FROM password_resets WHERE token='$token' AND created_at > (NOW() - INTERVAL 1 HOUR)"; $result = mysqli_query($db, $query); if (mysqli_num_rows($result) == 0) { array_push($errors, "This reset link has expired or is invalid."); } // Verify the hashed token if (password_verify($token, $hashed_token)) { // Proceed with password reset logic } |
3. Update the new_pass.php
Page
We need to modify new_pass.php
to display a success or error message when the password has been successfully reset.
1 2 3 4 5 6 7 8 9 10 | // Add a success message after the password has been reset successfully if (isset($_GET['status']) && $_GET['status'] == 'success') { echo "<div class='msg success-msg'>Your password has been successfully updated!</div>"; } else if (isset($errors) && count($errors) > 0) { foreach ($errors as $error) { echo "<div class='msg error-msg'>$error</div>"; } } |
4. Add a Password Strength Validator
You can also add a password strength validator to ensure users choose a secure password.
Here’s a simple JavaScript password strength meter that you can use:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <div class="form-group"> <label>New Password</label> <input type="password" name="new_pass" id="new_pass" onkeyup="checkPasswordStrength()"> <div id="password-strength" class="strength-meter"></div> </div> <script> function checkPasswordStrength() { var password = document.getElementById("new_pass").value; var strength = 0; var strengthMeter = document.getElementById("password-strength"); // Check for length if (password.length >= 8) strength += 1; // Check for numbers if (/[0-9]/.test(password)) strength += 1; // Check for uppercase letters if (/[A-Z]/.test(password)) strength += 1; // Check for special characters if (/[^A-Za-z0-9]/.test(password)) strength += 1; // Display strength if (strength == 1) { strengthMeter.innerText = "Weak"; strengthMeter.style.color = "red"; } else if (strength == 2) { strengthMeter.innerText = "Moderate"; strengthMeter.style.color = "orange"; } else if (strength == 3) { strengthMeter.innerText = "Strong"; strengthMeter.style.color = "green"; } else { strengthMeter.innerText = "Very Weak"; strengthMeter.style.color = "gray"; } } </script> |
This JavaScript checks the password strength as the user types and provides feedback in real-time. You can further customize the strength rules and style it according to your preferences.
Summary of Changes
- Token Expiry and Hashing: We introduced token expiry to ensure that the reset link expires after a certain period (1 hour in this example). Additionally, we hash the token for enhanced security.
- Password Strength Validation: We added a simple password strength validator to encourage users to set strong passwords.
- Success/Error Messages: After the password is reset, the system will show a confirmation message. If there are any issues, error messages will guide the user to fix them.
- Redirect After Email Sent: Users are redirected to a confirmation page after they request a password reset email.
Next Steps
To further improve the system, you could implement:
- Rate Limiting: To prevent abuse, implement a rate-limiting system that limits the number of password reset requests per email address in a certain time frame.
- 2FA (Two-Factor Authentication): Adding two-factor authentication for password resets can further secure the process.
- Email Template Design: Customize the email template to include more details or branding for your website, making the email look professional.
By implementing these changes, you’ll have a robust and secure password reset system that offers a smooth user experience while ensuring security.