week6-authentication-system

Authentication System — Week 6: Backend Integration & Authentication

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.


1. Tech Stack

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.


2. Features

Required

Bonus implemented


3. Folder Structure

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

4. Database Schema

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:


5. Setup

  1. Copy the auth-system folder into C:\laragon\www\
  2. Open Laragon and press Start All (Apache + MySQL must be green)
  3. Import the database — either:
    • open phpMyAdmin (http://localhost/phpmyadmin) → Import → database/schema.sql, or
    • run from a terminal:
      C:\laragon\bin\mysql\mysql-8.4.3-winx64\bin\mysql.exe -u root < database/schema.sql
      
  4. If your MySQL root user has a password, set it in config/db.php (DB_PASS)
  5. Visit http://localhost/auth-system/

Default Laragon credentials are user root with an empty password.


6. How Authentication Works

6.1 Signup

  1. The browser checks the fields first (assets/js/validation.js) — this is only for speed and comfort.
  2. The form POSTs to signup.php with a hidden CSRF token.
  3. PHP re-validates everything server-side, because JavaScript can be disabled:
    • name present, 3–100 characters, letters only
    • email valid via filter_var($email, FILTER_VALIDATE_EMAIL)
    • password 6–72 characters and matching the confirmation
  4. The email is looked up; if it already exists the form comes back with an error.
  5. The password is hashed:
    $hash = password_hash($password, PASSWORD_DEFAULT);   // bcrypt, salt generated automatically
    

    The plaintext password is never written to the database or to any log.

  6. The row is inserted with a prepared statement. If two people register the same email at the same instant, the UNIQUE index raises SQLSTATE 23000 and that is caught and shown as a normal form error.
  7. The user is redirected to the login page with a success message.

6.2 Login

  1. login.php receives the email and password over POST, with a CSRF token.
  2. The user row is fetched by email using a prepared statement.
  3. The password is checked with:
    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.

  4. If the credentials are wrong, the message is always the same generic “Invalid email or password”. Saying “no such user” would let an attacker discover which email addresses are registered (user enumeration).
  5. After 5 failed attempts the session is locked out for 60 seconds.
  6. On success:
    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.

  7. The user is redirected to the dashboard.

6.3 Session & access control

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).

6.4 Logout

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:

  1. empties $_SESSION
  2. expires the session cookie in the browser
  3. calls session_destroy() to delete the session file on the server

6.5 Forgot password (bonus)

  1. The user submits their email. The response is always the same (“if an account exists, a link has been sent”), again to prevent user enumeration.
  2. If the account does exist, a 64-character random token is generated with random_bytes() (cryptographically secure).
  3. The SHA-256 hash of that token is stored in 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.
  4. reset-password.php hashes the incoming token, finds the matching non-expired row, and lets the user set a new password.
  5. On success the new hash is saved and reset_token is set to NULL, making the link single-use.

There is no mail server on a local Laragon machine, so DEMO_MODE in forgot-password.php prints the reset link on screen instead of emailing it. Set DEMO_MODE = false and send the link with PHPMailer or mail() in production.


7. Security Checklist

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

8. Testing Checklist


9. Screenshots

Place the images in screenshots/ and they will render here.

Page Screenshot
Signup ![Signup](screenshots/signup.png)
Login ![Login](screenshots/login.png)
Dashboard ![Dashboard](screenshots/dashboard.png)

10. Possible Next Steps