Authentication code often starts with two form fields and a database query. That simplicity can be misleading. A login system also has to protect stored passwords, prevent SQL injection, manage sessions safely and enforce permissions after a user signs in.
This guide presents a practical foundation for a PHP and MySQL login system. The examples use PDO, PHP’s password-hashing functions and server-side sessions. You can adapt the structure for a custom PHP application, client portal, membership site or internal dashboard.
Plan the Authentication Flow
Before writing the first query, decide which responsibilities belong to the login system and which belong to the wider application. A typical implementation includes:
- A
userstable containing account details and password hashes. - A registration process that validates input and creates accounts.
- A login process that retrieves an account and verifies its password.
- Protected pages that check the session before returning private data.
- A logout process that invalidates the active session.
Production systems also need a password-reset flow, email verification where appropriate, CSRF protection, rate limiting and role-based authorization. Checking whether someone is logged in does not, by itself, determine what that person is allowed to do.
Create the MySQL Users Table
Passwords should never be stored as plain text. Do not use reversible encryption or outdated general-purpose hashes such as MD5. Store the result of PHP’s password_hash() function instead. The resulting value contains the information PHP needs for a later verification.
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(190) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(30) NOT NULL DEFAULT 'customer',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
The unique index prevents two accounts from using the same email address. A VARCHAR(255) column gives the application room for hashes produced by current and future PHP password algorithms.
Connect to MySQL with PDO
Database credentials should not be placed in publicly accessible files. Environment variables are a useful option. On smaller projects, a configuration file outside the web root can also work, provided the server prevents direct access to it.
<?php
$dsn = 'mysql:host=localhost;dbname=example_app;charset=utf8mb4';
$dbUser = getenv('DB_USER');
$dbPassword = getenv('DB_PASSWORD');
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO($dsn, $dbUser, $dbPassword, $options);
Prepared statements keep user input separate from SQL instructions. They are essential for preventing SQL injection, but only when you actually bind values rather than concatenating request data into a query.
Register Users with Password Hashing
Registration should validate the email address, apply a password policy and handle duplicate accounts gracefully. Normalize the email address consistently, but do not silently modify the password a user submits.
<?php
$email = strtolower(trim($_POST['email'] ?? ''));
$name = trim($_POST['name'] ?? '');
$password = $_POST['password'] ?? '';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
exit('Please enter a valid email address.');
}
if (strlen($password) < 12) {
exit('Use a password with at least 12 characters.');
}
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare(
'INSERT INTO users (name, email, password_hash) VALUES (:name, :email, :password_hash)'
);
$stmt->execute([
'name' => $name,
'email' => $email,
'password_hash' => $passwordHash,
]);
The database constraint remains important even if the registration form checks whether an email already exists. Two requests can arrive at nearly the same time, so the unique index is the final safeguard. In production, catch a duplicate-key exception and show a controlled message instead of exposing database details.
Verify Login Credentials
During login, retrieve the account by email and compare the submitted password with the stored hash using password_verify(). Never log or display the password.
<?php
session_start();
$email = strtolower(trim($_POST['email'] ?? ''));
$password = $_POST['password'] ?? '';
$stmt = $pdo->prepare(
'SELECT id, name, email, password_hash, role
FROM users
WHERE email = :email
LIMIT 1'
);
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
exit('The email or password is incorrect.');
}
session_regenerate_id(true);
$_SESSION['user_id'] = (int) $user['id'];
$_SESSION['user_name'] = $user['name'];
$_SESSION['user_role'] = $user['role'];
$_SESSION['logged_in_at'] = time();
echo 'Login successful.';
Use the same general message for an unknown email and an incorrect password. This avoids unnecessarily revealing which email addresses have accounts. Add rate limiting, increasing delays or temporary protection after repeated failures rather than relying on the error message alone.
Configure and Protect Sessions
Set important cookie attributes before calling session_start():
<?php
session_set_cookie_params([
'httponly' => true,
'secure' => true,
'samesite' => 'Lax',
]);
session_start();
The secure attribute requires HTTPS. Use HTTPS throughout the authenticated area, not just on the login form. HttpOnly prevents ordinary JavaScript from reading the session cookie, while SameSite helps limit some cross-site request scenarios.
Regenerating the session ID immediately after a successful login helps reduce session fixation risk. For applications handling sensitive information, consider both idle and absolute session timeouts.
Require Authentication on Private Pages
Every private page and sensitive endpoint should perform its own server-side check:
<?php
session_start();
if (empty($_SESSION['user_id'])) {
header('Location: /login.php');
exit;
}
$userId = (int) $_SESSION['user_id'];
Do not rely on hidden buttons, client-side JavaScript or a URL that is difficult to guess. Those measures do not enforce access control. The server must check authentication and authorization for every operation, including viewing customer records, editing content and changing account settings.
Check roles on the server
A logged-in customer should not automatically have administrator permissions. For an administrative action, load the current user’s role or use a trusted session value and reject requests that do not meet the required permission.
<?php
if (($_SESSION['user_role'] ?? '') !== 'admin') {
http_response_code(403);
exit('Access denied.');
}
For larger applications, use a clear permission model rather than scattering role strings throughout the code.
Implement Logout
Logout should clear the server-side session and expire the session cookie where appropriate.
<?php
session_start();
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
session_destroy();
header('Location: /login.php');
exit;
For a modern application, make sure the cookie attributes used when deleting the cookie match those used when creating it. After logout, protected endpoints must still reject requests even if a visitor uses the browser’s Back button.
Security Measures Commonly Missed
Protect state-changing forms from CSRF
Login, registration, password changes and other state-changing requests should include a CSRF token tied to the user’s session. The server should verify the token before processing the request. SameSite cookies provide useful additional protection, but they should not replace a deliberate CSRF design for sensitive actions.
Escape values when rendering HTML
Prepared statements protect SQL queries; they do not protect HTML output. Escape names and other database values when inserting them into a page:
<?= htmlspecialchars($user['name'], ENT_QUOTES, 'UTF-8') ?>
Keep error messages and logs safe
Show visitors a controlled error message and log technical details privately. Never record passwords, session IDs or reset tokens. Authentication logs can include useful information such as the account identifier, timestamp and result, provided the data is handled responsibly.
Update the application stack
Use a supported PHP version, update dependencies and remove unused packages. If this system is being added to WordPress, review these practical WordPress security measures as part of the wider site review.
Test the Login System
Test both the expected path and the cases that should fail:
- Confirm that only password hashes, never plain-text passwords, are stored.
- Test empty fields, malformed emails and unusually long input.
- Submit SQL metacharacters in the email field and verify that the query remains safe.
- Confirm that the session ID changes after a successful login.
- Verify that logged-out visitors are redirected from every private page.
- Check that logout invalidates access to protected endpoints.
- Test accounts with different roles against each sensitive action.
- Confirm that the complete authentication flow runs over HTTPS.
- Test repeated failed logins and confirm that rate-limiting behavior is appropriate.
Frequently Asked Questions
Should I use MD5 or SHA-256 for passwords?
No. These are general-purpose hash functions and are not designed for password storage. Use password_hash() and verify passwords with password_verify().
Are PDO prepared statements enough to secure a login system?
No. They address SQL injection, but a complete implementation also needs password hashing, HTTPS, secure session settings, CSRF protection, output escaping, rate limiting and server-side authorization.
Should the logged-in user ID be stored in a cookie?
Prefer a server-managed session with a secure cookie containing only the session identifier. Do not put passwords, roles or sensitive account data in a client-controlled cookie.
Can this approach be used for an administrator dashboard?
It can provide the authentication foundation, but an administrator dashboard needs explicit permission checks and stronger protections. Consider multi-factor authentication and reauthentication for high-risk actions such as changing payment details or creating new administrators.
What should happen when a password hash needs updating?
PHP can indicate that a stored hash uses an outdated algorithm or cost through password_needs_rehash(). After a successful login, you can create a new hash and update the database when that check returns true.
Conclusion
A dependable PHP and MySQL login system is the result of several coordinated safeguards rather than one special query. Use prepared statements, modern password hashing, secure cookies, session regeneration, HTTPS and server-side permission checks as the foundation. Before deployment, add CSRF protection, rate limiting, password recovery, suitable timeouts and careful monitoring for the application’s risk level.
