A complete user authentication module built with PHP + MySQL on the backend and plain HTML / CSS / JavaScript on the frontend. It covers registration, login, session-based access control, a protected dashboard, and password reset.
| Layer | Technology | Why |
|---|---|---|
| Frontend | HTML5, CSS3, Vanilla JavaScript | No framework needed; keeps the logic visible and easy to explain |
| Backend | PHP 8.3 (procedural + helper functions) | Runs directly on Laragon/XAMPP with no build step |
| Database | MySQL 8.4 | Relational storage with a UNIQUE constraint on email |
| DB driver | PDO with prepared statements | Parameter binding makes SQL injection impractical |
| Hashing | password_hash() / password_verify() (bcrypt) |
PHP’s built-in bcrypt; salting is automatic |
| Sessions | PHP native $_SESSION (cookie-based) |
Server-side session state, ID regenerated on login |
| Local server | Laragon (Apache + MySQL) | Zero-config local stack |
No paid tools, no external libraries — everything used here ships with PHP itself.
Required
UNIQUE index)Bonus implemented
session_regenerate_id)auth-system/
├── index.php Entry point — routes to login or dashboard
├── signup.php Registration form + insert logic
├── login.php Login form + credential check
├── dashboard.php Protected page (requires an active session)
├── logout.php Destroys the session (POST + CSRF only)
├── forgot-password.php Issues a password reset token
├── reset-password.php Consumes the token, sets a new password
│
├── config/
│ └── db.php PDO connection settings
│
├── includes/
│ ├── init.php Bootstrap: session + DB + helpers
│ ├── functions.php Validation, CSRF, flash messages, auth guards
│ ├── header.php Shared page head + flash toast
│ └── footer.php Shared page footer
│
├── assets/
│ ├── css/style.css All styling (responsive, dark mode)
│ └── js/validation.js Client-side validation and UI helpers
│
├── database/
│ └── schema.sql Database + users table
│
└── screenshots/ Submission screenshots
CREATE TABLE `users` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`email` VARCHAR(190) NOT NULL,
`password` VARCHAR(255) NOT NULL, -- bcrypt hash, 60 chars
`reset_token` VARCHAR(64) DEFAULT NULL,
`reset_expires` DATETIME DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_users_email` (`email`)
) ENGINE=InnoDB;
Notes:
email is VARCHAR(190) so the UNIQUE index fits inside MySQL’s utf8mb4 key limit.password is VARCHAR(255); bcrypt produces 60 characters, the extra room is for future algorithms.reset_token stores the SHA-256 hash of the reset token, not the token itself.auth-system folder into C:\laragon\www\http://localhost/phpmyadmin) → Import → database/schema.sql, orC:\laragon\bin\mysql\mysql-8.4.3-winx64\bin\mysql.exe -u root < database/schema.sql
config/db.php (DB_PASS)Default Laragon credentials are user root with an empty password.
assets/js/validation.js) — this is only for speed and comfort.signup.php with a hidden CSRF token.filter_var($email, FILTER_VALIDATE_EMAIL)$hash = password_hash($password, PASSWORD_DEFAULT); // bcrypt, salt generated automatically
The plaintext password is never written to the database or to any log.
UNIQUE index raises SQLSTATE 23000 and that is caught and
shown as a normal form error.login.php receives the email and password over POST, with a CSRF token.password_verify($password, $user['password'])
password_verify() pulls the salt and cost out of the stored hash, re-hashes the input,
and compares the result in constant time — so response timing leaks nothing.
session_regenerate_id(true); // new session ID — defeats session fixation
$_SESSION['user_id'] = $user['id'];
Only the user’s ID and display name go into the session — never the password hash.
Every protected page begins with:
require_once __DIR__ . '/includes/init.php';
require_login();
require_login() checks for $_SESSION['user_id']. If it is missing, the visitor is
redirected to login.php before any HTML is printed, so the dashboard’s markup never
reaches a guest. Typing /dashboard.php directly in the address bar therefore just
bounces to the login page.
The session cookie is created with httponly (JavaScript cannot read it) and
SameSite=Lax (it is not sent on cross-site POSTs).
logout.php accepts POST only, with a valid CSRF token — otherwise any other website
could log our users out with a hidden <img src="...logout.php"> tag. It then:
$_SESSIONsession_destroy() to delete the session file on the serverrandom_bytes() (cryptographically secure).reset_token with a 1-hour expiry.
The raw token only ever exists in the link — so even a stolen database cannot be used
to reset anyone’s password.reset-password.php hashes the incoming token, finds the matching non-expired row,
and lets the user set a new password.reset_token is set to NULL, making the link
single-use.There is no mail server on a local Laragon machine, so
DEMO_MODEinforgot-password.phpprints the reset link on screen instead of emailing it. SetDEMO_MODE = falseand send the link with PHPMailer ormail()in production.
| Threat | Defence |
|---|---|
| Password theft from a database leak | bcrypt via password_hash(), unique salt per user |
| SQL injection | PDO prepared statements everywhere, ATTR_EMULATE_PREPARES => false |
| XSS | Every echoed value passes through e() → htmlspecialchars() |
| CSRF | Random token in the session, hidden in every form, compared with hash_equals() |
| Session fixation | session_regenerate_id(true) on login |
| Session cookie theft via XSS | httponly + SameSite=Lax cookie flags |
| Duplicate accounts | App-level check plus a UNIQUE index on email |
| Brute-force login | 5 attempts, then a 60-second lockout |
| User enumeration | Identical responses for unknown email and wrong password |
| Bypassing validation | Every JavaScript rule is duplicated in PHP |
| Credential leakage in errors | DB exceptions are logged, never displayed |
notanemail → “Please enter a valid email address”users table in phpMyAdmin → password column is a $2y$... hash, not plaintexthttp://localhost/auth-system/dashboard.php while logged out → bounced to loginPlace the images in screenshots/ and they will render here.
| Page | Screenshot |
|---|---|
| Signup |  |
| Login |  |
| Dashboard |  |