In this article · 8 sections
Almost every custom website needs an admin area: bookings, enquiries, blog posts. A PHP login system done wrong is one of the easiest ways for a site to get hacked. Here's a secure, simple pattern.
1. Users table
CREATE TABLE admins (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(190) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
failed_logins INT NOT NULL DEFAULT 0,
locked_until DATETIME NULL
);
2. Create a user with a hashed password
<?php
$hash = password_hash('a-long-unique-passphrase', PASSWORD_DEFAULT);
// INSERT the email and $hash using a prepared statement
Never store plain passwords or use md5()/sha1() for passwords.
3. Secure session settings
<?php
session_set_cookie_params([
'httponly' => true,
'secure' => true, // HTTPS only
'samesite' => 'Lax',
]);
session_start();
4. The login handler
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) exit('Invalid request');
$email = trim($_POST['email'] ?? '');
$pass = $_POST['password'] ?? '';
$stmt = $conn->prepare('SELECT id, password_hash, failed_logins, locked_until FROM admins WHERE email = ?');
$stmt->bind_param('s', $email);
$stmt->execute();
$user = $stmt->get_result()->fetch_assoc();
if ($user && $user['locked_until'] && strtotime($user['locked_until']) > time()) {
$error = 'Too many attempts. Try again later.';
} elseif ($user && password_verify($pass, $user['password_hash'])) {
session_regenerate_id(true);
$_SESSION['admin_id'] = $user['id'];
$conn->query('UPDATE admins SET failed_logins = 0, locked_until = NULL WHERE id = ' . (int)$user['id']);
header('Location: /admin/dashboard.php');
exit;
} else {
if ($user) {
$stmt = $conn->prepare('UPDATE admins SET failed_logins = failed_logins + 1,
locked_until = IF(failed_logins + 1 >= 5, NOW() + INTERVAL 15 MINUTE, NULL) WHERE id = ?');
$stmt->bind_param('i', $user['id']);
$stmt->execute();
}
$error = 'Invalid email or password.';
}
}
$_SESSION['csrf'] = $_SESSION['csrf'] ?? bin2hex(random_bytes(32));
In the form, include <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">.
Notes:
- The same error message for wrong email or password avoids revealing which emails exist
session_regenerate_id(true)prevents session fixation- The lockout slows brute-force attacks. See brute force attacks
5. Protect admin pages
<?php
session_start();
if (empty($_SESSION['admin_id'])) {
header('Location: /admin/login.php');
exit;
}
Put this at the top of every admin page (or in a shared include).
6. Logout
<?php
session_start();
$_SESSION = [];
session_destroy();
header('Location: /admin/login.php');
Extra hardening
- HTTPS only
- 2FA for admins (TOTP apps) on important systems
- Log logins with IP and time
- Rehash passwords with
password_needs_rehash()when algorithms update
FAQs
Should I build my own login or use a framework?
Frameworks like Laravel include well-tested authentication. For small custom sites, the pattern above is solid.
How long should sessions last?
For admin panels, expire after inactivity (e.g. 30–60 minutes).
Was this article helpful?
Your answer helps us improve our guides