Forgot Password Reset in PHP
In this lesson: Secure forgot-password flow in PHP — token, expiry, email reset link and new password update — full code.
How password reset works
Since passwords are stored only as hashes, you cannot "look up" a forgotten one. Instead, a forgot-password flow proves the user controls their email and lets them set a new password. It uses a one-time token: request → email a link → verify token → set new password.
Step 1: Request a reset
<?php
$email = trim($_POST["email"]);
// check the email exists in users table (prepared statement)
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
?>
The user submits their email. We look it up with a prepared statement. If it exists, we move on to create a reset token.
Store the token with expiry
<?php
$token = bin2hex(random_bytes(32)); // secure random token
$expiry = date("Y-m-d H:i:s", time() + 3600); // 1 hour
$stmt = $pdo->prepare(
"UPDATE users SET reset_token = ?, reset_expiry = ? WHERE email = ?"
);
$stmt->execute([$token, $expiry, $email]);
?>
We generate a long random token with random_bytes and save it in the database with a one-hour expiry. Storing it server-side lets us verify it later.
Email the reset link
<?php
$link = "https://yoursite.com/reset.php?token=$token";
$body = "Click to reset your password (valid 1 hour):\n$link";
// send $body by email (PHPMailer / SMTP)
?>
We email a link containing the token. When the user clicks it, they land on the reset page with the token in the URL, ready to be verified.
Step 2: Set a new password
<?php
$token = $_GET["token"] ?? "";
$stmt = $pdo->prepare(
"SELECT id FROM users WHERE reset_token = ? AND reset_expiry > NOW()"
);
$stmt->execute([$token]);
$user = $stmt->fetch();
if ($user) {
$newHash = password_hash($_POST["password"], PASSWORD_DEFAULT);
$upd = $pdo->prepare(
"UPDATE users SET password = ?, reset_token = NULL WHERE id = ?"
);
$upd->execute([$newHash, $user["id"]]);
echo "Password updated";
} else {
echo "Invalid or expired link";
}
?>
We accept the token only if it matches and has not expired (reset_expiry > NOW()). The new password is hashed with password_hash, and the token is cleared so it cannot be reused.
Common mistakes
- Emailing the actual password. You cannot — it is only a hash. Email a token link.
- Weak tokens. Use
random_bytes, notrand()ormd5(time()). - No expiry. Always set and check an expiry on the token.
- Not clearing the token. Remove it after use so the link cannot be reused.
Summary
- You cannot recover a hashed password — you reset it via a token.
- Generate a secure token with
random_bytesand store it with an expiry. - Email a link containing the token; never email the password.
- Verify the token and expiry, then set a new hashed password.
- Clear the token after use so the link is one-time.
Next, understand hashing in password hashing, or build the login in login and registration.
Frequently Asked Questions
How does a forgot-password flow work?
Why use a token instead of emailing the password?
Why must the reset token expire?
How do I generate a secure token?
bin2hex(random_bytes(32)) for a long, unpredictable token. Avoid weak functions like rand() or md5(time()), which are guessable.Should the new password be hashed?
password_hash() before saving, exactly like at signup. The database should never hold a plain password.