A registration and login system is the first real feature most PHP projects need, and the one most often built insecurely. This guide builds a small, complete version with PHP 8 and MySQL: sign-up, login, a members-only page and logout, using password hashing, prepared statements, CSRF tokens and secure sessions.
Store passwords only with password_hash() and check them with password_verify(). Talk to MySQL through PDO prepared statements, never by pasting user input into SQL. Protect every form with a CSRF token, call session_regenerate_id(true) at login, and keep your database password in a config file outside the web root. The complete code is below, in six short files.
1. What you will build
The files use three helpers: config.php for settings, db.php for the database connection, and csrf.php for sessions and CSRF tokens.
2. Create the database and table
In cPanel, open Database Wizard: create a database, create a user with a strong password, and give that user All Privileges on the database. DirectAdmin has an equivalent screen for creating databases and users. Note the full names, which usually start with your account username, such as youruser_app.
Then open phpMyAdmin, select the database, and run this on the SQL tab:
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_users_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;The unique key means the database itself refuses a second account with the same email, even if two sign-ups arrive at the same moment. password_hash is 255 characters so that stronger hashing algorithms fit later.
3. Configuration outside the web root
Create a folder next to public_html, not inside it, for example /home/youruser/app/. Files there cannot be downloaded through the website, even if PHP stops working.
/home/youruser/app/config.php:
<?php
return [
'db_dsn' => 'mysql:host=localhost;dbname=youruser_app;charset=utf8mb4',
'db_user' => 'youruser_appuser',
'db_pass' => 'a-long-random-password',
];Set its permissions to 600 in File Manager, and never commit it to Git.
/home/youruser/app/db.php:
<?php
declare(strict_types=1);
function db(): PDO
{
static $pdo = null;
if ($pdo === null) {
$c = require __DIR__ . '/config.php';
$pdo = new PDO($c['db_dsn'], $c['db_user'], $c['db_pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return $pdo;
}Exceptions mean a failed query stops the script instead of carrying on silently. Real prepared statements (EMULATE_PREPARES => false) send the SQL and the data to MySQL separately, so input can never change the query.
4. Sessions and CSRF protection
Save the helper from Understanding and implementing CSRF tokens in PHP forms as /home/youruser/app/csrf.php. It gives you four functions used below:
start_secure_session()starts the session withSecure,HttpOnlyandSameSite=Laxcookies;csrf_field()prints the hidden token field in a form;csrf_verify()rejects a POST without the right token;csrf_token()returns the token itself.
Add one small helper to the end of the same file, for escaping output:
function e(string $v): string
{
return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
}5. Registration
public_html/register.php:
<?php
declare(strict_types=1);
require '/home/youruser/app/db.php';
require '/home/youruser/app/csrf.php';
start_secure_session();
$errors = [];
$email = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify();
$email = trim((string) ($_POST['email'] ?? ''));
$password = (string) ($_POST['password'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > 255) {
$errors[] = 'Please enter a valid email address.';
}
if (strlen($password) < 12 || strlen($password) > 72) {
$errors[] = 'Use a password between 12 and 72 characters.';
}
if (!$errors) {
try {
$stmt = db()->prepare('INSERT INTO users (email, password_hash) VALUES (?, ?)');
$stmt->execute([strtolower($email), password_hash($password, PASSWORD_DEFAULT)]);
header('Location: login.php?registered=1');
exit;
} catch (PDOException $ex) {
if ($ex->errorInfo[1] === 1062) { // duplicate email
$errors[] = 'That email is already registered. Try logging in.';
} else {
error_log($ex->getMessage());
$errors[] = 'Something went wrong. Please try again later.';
}
}
}
}
?>
<h1>Create an account</h1>
<?php foreach ($errors as $err): ?><p class="error"><?= e($err) ?></p><?php endforeach; ?>
<form method="post">
<?= csrf_field() ?>
<label>Email <input type="email" name="email" value="<?= e($email) ?>" required></label>
<label>Password <input type="password" name="password" minlength="12" maxlength="72" autocomplete="new-password" required></label>
<button type="submit">Register</button>
</form>Points worth knowing:
PASSWORD_DEFAULTis bcrypt today; PHP can move it to a stronger algorithm in a future version, and the login code below upgrades old hashes when that happens. Never usemd5()orsha1()for passwords.- The 72-character cap exists because bcrypt ignores everything after 72 bytes. Long passphrases below that are fine.
- Database errors are logged, not shown. Printing
$ex->getMessage()to visitors, as many tutorials do, leaks table names and sometimes credentials.
6. Login
public_html/login.php:
<?php
declare(strict_types=1);
require '/home/youruser/app/db.php';
require '/home/youruser/app/csrf.php';
start_secure_session();
$error = '';
$email = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify();
$email = strtolower(trim((string) ($_POST['email'] ?? '')));
$password = (string) ($_POST['password'] ?? '');
$stmt = db()->prepare('SELECT id, password_hash FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
db()->prepare('UPDATE users SET password_hash = ? WHERE id = ?')
->execute([password_hash($password, PASSWORD_DEFAULT), $user['id']]);
}
session_regenerate_id(true);
unset($_SESSION['csrf_token']);
$_SESSION['user_id'] = (int) $user['id'];
header('Location: account.php');
exit;
}
$error = 'Incorrect email or password.';
}
?>
<h1>Log in</h1>
<?php if (isset($_GET['registered'])): ?><p>Account created. Please log in.</p><?php endif; ?>
<?php if ($error): ?><p class="error"><?= e($error) ?></p><?php endif; ?>
<form method="post">
<?= csrf_field() ?>
<label>Email <input type="email" name="email" value="<?= e($email) ?>" required></label>
<label>Password <input type="password" name="password" autocomplete="current-password" required></label>
<button type="submit">Log in</button>
</form>- One message for both failures. "Incorrect email or password" does not tell an attacker which emails are registered.
session_regenerate_id(true)gives the user a new session ID at login, which defeats session fixation. The CSRF token is cleared so a fresh one is made for the new session.password_needs_rehash()upgrades old hashes quietly the next time each user logs in.
7. The members-only page and logout
public_html/account.php:
<?php
declare(strict_types=1);
require '/home/youruser/app/csrf.php';
start_secure_session();
if (empty($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
?>
<h1>Your account</h1>
<form method="post" action="logout.php">
<?= csrf_field() ?>
<button type="submit">Log out</button>
</form>public_html/logout.php:
<?php
declare(strict_types=1);
require '/home/youruser/app/csrf.php';
start_secure_session();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
csrf_verify();
$_SESSION = [];
$p = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 3600,
'path' => $p['path'],
'domain' => $p['domain'],
'secure' => $p['secure'],
'httponly' => $p['httponly'],
'samesite' => $p['samesite'],
]);
session_destroy();
header('Location: login.php');
exit;Logout is a POST with a token, so another website cannot log your users out with a hidden link. The session data, the cookie and the server-side session are all cleared. More on session handling is in Master PHP sessions.
8. Before you go live
A public sign-up form attracts bots and password-guessing. Before launch, add a limit on failed logins per email and per IP address (for example, a table of attempts and a short lock-out), email verification with a random, expiring token, and a password-reset flow that uses the same kind of token. Serve every page over HTTPS.
Also read Preventing SQL injection in PHP and Node.js and Preventing XSS in PHP and Node.js. If you would rather not build authentication yourself, a framework such as Laravel includes registration, login, password reset and email verification in its starter kits.
9. Running this on Domain India
The code runs as it is on Domain India cPanel and DirectAdmin shared hosting. The database host is localhost. On cPanel, new accounts start on PHP 8.3 and you can change the version per domain (measured September 2026); free SSL is included, which the Secure cookie needs. For sign-up confirmation emails, PHP's mail() with the -f envelope sender works on our cPanel servers, where SMTP libraries fail because socket functions are disabled; see PHP sendmail settings.
MySQL port 3306 is closed from the internet on our shared servers, so manage the database with phpMyAdmin or through an SSH tunnel; see How to connect to the MySQL database. The card shows the live monthly price, excluding 18% GST.
- 25 GB NVMe SSD Storage
- 50 GB Monthly Bandwidth
- 1 Website
- 10 Email Accounts
How should I store passwords in PHP?
Use password_hash($password, PASSWORD_DEFAULT) when the user registers and password_verify() when they log in. Store the whole hash in a VARCHAR(255) column. Never store plain passwords, and never use md5() or sha1() for passwords.
How do I stop SQL injection in a PHP login form?
Use PDO or mysqli prepared statements with placeholders, and pass user input as parameters to execute(). With PDO, also set ATTR_EMULATE_PREPARES to false. Never build SQL by joining strings with user input.
Why should I call session_regenerate_id() after login?
It gives the user a new session ID the moment they log in, so an ID that an attacker planted or saw before login no longer works. Pass true so the old session is deleted.
Should the login error say whether the email exists?
No. Use one message such as "Incorrect email or password" for both cases, so the form cannot be used to find out which email addresses have accounts.
Where should I keep my database password?
In a config file outside public_html, with permissions set to 600, loaded with require. Files outside the web root cannot be downloaded through the website, even if PHP stops working.
Do I need CSRF tokens on login and logout forms?
Yes. Protect every form that changes state, including login, registration and logout, with a CSRF token. Make logout a POST request, so another website cannot log your users out with a link.
Ready to build it? Host your project on cPanel hosting or DirectAdmin hosting, and read CSRF tokens in PHP forms first.
MySQL databases with phpMyAdmin, a choice of PHP versions per domain and free SSL on every plan.
See cPanel plans