Step 0: store passwords correctly. Never store plain-text passwords, and never use MD5 or SHA1 for passwords. Use PHP's built-in password hashing, which adds a random salt and uses a slow algorithm:
PHP
// Registration
$hash = password_hash($_POST['password'], PASSWORD_DEFAULT); // about 60 to 255 characters
$pdo->prepare('INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)')
->execute([$name, $email, $hash]);
// Login
$stmt = $pdo->prepare('SELECT id, password_hash, role FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($_POST['password'], $user['password_hash'])) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['role'] = $user['role'];
header('Location: index.php'); exit;
}
$error = 'Invalid email or password'; // same message whether email or password was wrongBecause a hash cannot be reversed, a website can never email you your old password. A proper "password reminder" sends a time-limited, single-use reset link.
Forgot-password flow
Table:
SQL
CREATE TABLE password_resets (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
token_hash CHAR(64) NOT NULL,
expires_at DATETIME NOT NULL,
used TINYINT(1) NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id)
);forgot.php (request step):
PHP
<?php
require 'config/db.php'; require 'includes/mail.php'; // mail.php: see 13.8
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
$stmt = $pdo->prepare('SELECT id FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user) {
$token = bin2hex(random_bytes(32)); // sent to user
$hash = hash('sha256', $token); // stored in database
$pdo->prepare('INSERT INTO password_resets (user_id, token_hash, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 30 MINUTE))')
->execute([$user['id'], $hash]);
$link = 'https://yourshop.com/reset.php?token=' . $token;
send_mail($email, 'Reset your password', 'Click to reset (valid 30 minutes): ' . $link);
}
// Same answer whether or not the email exists (prevents account discovery)
$message = 'If that email is registered, a reset link has been sent.';
}reset.php (confirm step):
PHP
<?php
require 'config/db.php';
$token = $_GET['token'] ?? $_POST['token'] ?? '';
$hash = hash('sha256', $token);
$stmt = $pdo->prepare('SELECT id, user_id FROM password_resets WHERE token_hash = ? AND used = 0 AND expires_at > NOW()');
$stmt->execute([$hash]);
$reset = $stmt->fetch();
if (!$reset) exit('This link is invalid or has expired.');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$new = $_POST['password'] ?? '';
if (strlen($new) < 8) { $error = 'Password must be 8+ characters'; }
else {
$pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?')
->execute([password_hash($new, PASSWORD_DEFAULT), $reset['user_id']]);
$pdo->prepare('UPDATE password_resets SET used = 1 WHERE id = ?')->execute([$reset['id']]);
$pdo->prepare('DELETE FROM auth_tokens WHERE user_id = ?')->execute([$reset['user_id']]); // sign out remembered devices (13.6)
header('Location: login.php?reset=1'); exit;
}
}
// ... show the form with a hidden input name='token' value='...'Additional good practice: rate-limit reset requests; send a "your password was changed" email; require a strong password; never log tokens.