PHP cookbook · Intermediate
Build a secure password reset flow in PHP
Let users reset forgotten passwords without enabling account takeover or email enumeration.
Approach
Generate a long random token, store only its hash with a short expiry (e.g., 30 minutes), email a link containing the raw token, and always show the same message whether the email exists or not. On reset, look up by hash, check expiry, set the new password with password_hash(), delete all reset tokens for the user and invalidate existing sessions.
Solution
<?php
declare(strict_types=1);
function requestReset(PDO $pdo, Mailer $mailer, string $email): void
{
$user = findUserByEmail($pdo, $email);
if ($user) {
$token = bin2hex(random_bytes(32));
$pdo->prepare('INSERT INTO password_resets (user_id, token_hash, expires_at) VALUES (?, ?, NOW() + INTERVAL \'30 minutes\')')
->execute([$user['id'], hash('sha256', $token)]);
$mailer->sendResetLink($user['email'], "https://example.com/reset?token={$token}");
}
// Same response either way (and similar timing — consider queueing the email)
}
function resetPassword(PDO $pdo, string $token, #[\SensitiveParameter] string $newPassword): bool
{
$stmt = $pdo->prepare('SELECT user_id FROM password_resets WHERE token_hash = ? AND expires_at > NOW()');
$stmt->execute([hash('sha256', $token)]);
$userId = $stmt->fetchColumn();
if (!$userId || strlen($newPassword) < 12) {
return false;
}
$pdo->beginTransaction();
$pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?')->execute([password_hash($newPassword, PASSWORD_DEFAULT), $userId]);
$pdo->prepare('DELETE FROM password_resets WHERE user_id = ?')->execute([$userId]);
$pdo->prepare('DELETE FROM sessions WHERE user_id = ?')->execute([$userId]); // log out everywhere
$pdo->commit();
return true;
}
Pitfalls
- Storing raw tokens (a DB leak lets attackers reset any account).
- 'No account with that email' messages enabling enumeration.
- Tokens that never expire or can be reused.
Variations
- Rate-limit reset requests per email and IP.
- Notify users by email after a successful password change.
Further reading: OWASP Forgot Password Cheat Sheet
Reference: https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html
More PHP recipes
- Add CSRF protection to PHP forms
- Add health check endpoints to a PHP app
- Add structured logging with request IDs in PHP
- Cache expensive queries with Redis in PHP
- Call external APIs with timeouts and retries in PHP
- Handle file uploads securely in PHP
- Implement rate limiting in a PHP API
- Make a PHP payment endpoint idempotent