Topic 7: Sessions, login page, logout (Session destroy, timeout, Cookies introduction, Page redirect)
📖 13 min read · 🎯 advanced · 🧭 Prerequisites: javascript-cookies, recreate-task-external-css
Why this matters
Up until now, your web pages have no memory — every time someone visits a page, the server treats them like a complete stranger. That's a problem the moment you want a login system. You need the server to remember: "Yes, this person signed in, let them through." That's exactly what PHP sessions and cookies do. In this lesson we'll build a real login page, protect pages so only logged-in users can see them, handle logout, and even kick users out if they've been idle too long. This is the line between a toy project and something that actually works in the real world.
What You'll Learn
- Create a MySQL
userstable that stores bcrypt-hashed passwords - Build a
login.phppage that authenticates users and starts a PHP session - Guard protected pages with a session check that redirects unauthenticated visitors
- Implement a 30-minute session timeout using
$_SESSION['LAST_ACTIVITY'] - Set and read a "remember me" cookie with
setcookie()and$_COOKIE - Destroy a session cleanly on logout and redirect back to the login page
The Analogy
Think of PHP sessions as the lanyard badge system at a secure office building. When you pass reception (the login page) and show valid ID (your username and password), the guard issues you a temporary badge (the session). Every time you swipe through a door (request a protected page), the reader checks that badge. After 30 minutes of inactivity the badge expires automatically. Cookies are the "remember me" sticker that lets reception recognise your face the next morning even before you fetch a new badge — they survive browser restarts, unlike the lanyard which disappears the moment you leave the building (session end).
Chapter 1: Database Setup
Before writing a single line of PHP, the data layer needs to be in place. the trainer insists on designing the foundation before the walls.
Create the database and the users table in MySQL:
CREATE DATABASE user_management;
USE user_management;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
INSERT INTO users (username, password) VALUES
('admin', '$2y$10$WzH/q9TvkhO92eB.k7B8ue4jeVRRTY7jbWnce7xxEr5EIKD1S/WPC'); -- password is 'password'
Key points about this schema:
idis an auto-incrementing primary key.usernamehas aUNIQUEconstraint — duplicate usernames are rejected at the database level.passwordstores 255 characters to comfortably hold any bcrypt hash (bcrypt output is 60 characters, but VARCHAR(255) future-proofs the column).- The inserted hash was produced with
password_hash('password', PASSWORD_BCRYPT). Never store plain-text passwords. Always hash withpassword_hash()and verify withpassword_verify().
flowchart LR
Browser -->|POST username+password| login.php
login.php -->|SELECT * FROM users WHERE username=?| MySQL[(user_management.users)]
MySQL -->|row with hashed password| login.php
login.php -->|password_verify passes| Session[(PHP Session)]
Session -->|$_SESSION set| welcome.php
welcome.php -->|logout link| logout.php
logout.php -->|session_destroy| login.php
Chapter 2: The Login Page (login.php)
The login page does two things in one file: it renders the HTML form on GET, and it processes credentials on POST.
<?php
session_start();
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "user_management";
// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username = '$username'";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
if (password_verify($password, $row['password'])) {
$_SESSION['username'] = $username;
header("Location: welcome.php");
exit();
} else {
echo "Invalid password.";
}
} else {
echo "No user found.";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<form method="post" action="">
Username: <input type="text" name="username" required><br>
Password: <input type="password" name="password" required><br>
<button type="submit">Login</button>
</form>
</body>
</html>
Walk-through of the critical lines:
| Line | Purpose |
|---|---|
session_start() | Must appear before any output; initialises or resumes a session |
$_SERVER["REQUEST_METHOD"] == "POST" | Separates form rendering (GET) from form processing (POST) |
password_verify($password, $row['password']) | Compares the plain-text input against the stored bcrypt hash |
$_SESSION['username'] = $username | Stores the authenticated username in the server-side session store |
header("Location: welcome.php") | Sends an HTTP redirect to the protected welcome page |
exit() | Stops script execution immediately after the redirect header — critical, otherwise code below would still run |
Chapter 3: The Protected Welcome Page (welcome.php)
Any page that requires authentication follows the same guard pattern at the very top: check for the session, redirect if absent.
<?php
session_start();
if (!isset($_SESSION['username'])) {
header("Location: login.php");
exit();
}
echo "Welcome, " . $_SESSION['username'];
?>
<!DOCTYPE html>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<p>You are logged in.</p>
<a href="logout.php">Logout</a>
</body>
</html>
The two-line guard (!isset + header + exit) is the minimum viable access control for any PHP-protected page. If $_SESSION['username'] is not set — either because the user never logged in or their session expired — they are silently redirected to login.php. They never see the page content.
Chapter 4: The Logout Page (logout.php)
Logout must do three things in order: clear session variables, destroy the session file on the server, and redirect.
<?php
session_start();
session_unset(); // Remove all session variables
session_destroy(); // Delete the session file from the server
header("Location: login.php");
exit();
?>
session_unset()empties the$_SESSIONsuperglobal array.session_destroy()deletes the underlying session file (or DB record, depending on your session handler) from the server. Without this step, the session data would persist on disk.- The combination of both is the correct and complete logout sequence.
Chapter 5: Session Timeout
A session that never expires is a security liability — a borrowed laptop, a shared computer, or an abandoned tab can give an attacker access indefinitely. The fix is a LAST_ACTIVITY timestamp checked on every protected page request.
Add this block to the top of both login.php and welcome.php, immediately after session_start():
session_start();
$timeout_duration = 1800; // 30 minutes in seconds
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY']) > $timeout_duration) {
session_unset();
session_destroy();
header("Location: login.php");
exit();
}
$_SESSION['LAST_ACTIVITY'] = time(); // Refresh the timestamp on every valid request
How it works:
- On each page load, compare the current Unix timestamp (
time()) against$_SESSION['LAST_ACTIVITY']. - If the gap exceeds
$timeout_duration(1800 seconds = 30 minutes), destroy the session and redirect. - Otherwise, update
$_SESSION['LAST_ACTIVITY']to the current time so the 30-minute window slides forward with each legitimate request.
stateDiagram-v2
[*] --> LoggedOut
LoggedOut --> LoggedIn : POST login, password_verify passes
LoggedIn --> LoggedIn : Request within 30 min (LAST_ACTIVITY updated)
LoggedIn --> LoggedOut : 30 min inactivity (session_destroy)
LoggedIn --> LoggedOut : User clicks Logout (session_destroy)
Chapter 6: Cookies — "Remember Me"
PHP sessions are tied to the browser process. Close the tab, close the browser, or restart the machine and the session is gone (the session ID cookie is typically a session cookie — it does not persist). Cookies let you bridge that gap by storing data directly in the user's browser with a configurable expiry.
Setting a cookie on successful login (add inside the password_verify success block in login.php):
// login.php — inside the password_verify success block
if (password_verify($password, $row['password'])) {
$_SESSION['username'] = $username;
// Set a cookie that expires in 30 days
setcookie("username", $username, time() + (86400 * 30), "/");
header("Location: welcome.php");
exit();
} else {
echo "Invalid password.";
}
setcookie() signature breakdown:
| Argument | Value | Meaning |
|---|---|---|
| Name | "username" | Cookie name, read back via $_COOKIE['username'] |
| Value | $username | The value stored in the browser |
| Expiry | time() + (86400 * 30) | 86400 seconds/day × 30 days = 30-day expiry |
| Path | "/" | Cookie sent on all paths of this domain |
Reading the cookie in welcome.php to restore the session automatically:
// welcome.php — add before the session username check
if (isset($_COOKIE['username']) && !isset($_SESSION['username'])) {
$_SESSION['username'] = $_COOKIE['username'];
}
This fallback means: if the browser has a valid "username" cookie but no active session (e.g., the server was restarted), restore the session from the cookie rather than forcing a new login. Combine this with the session timeout logic for a complete, layered authentication system.
Chapter 7: Putting It All Together
Here is the full call sequence across all four files:
sequenceDiagram
participant B as Browser
participant L as login.php
participant W as welcome.php
participant O as logout.php
participant DB as MySQL
B->>L: GET /login.php (render form)
B->>L: POST username + password
L->>DB: SELECT * FROM users WHERE username = ?
DB-->>L: user row
L->>L: password_verify()
L->>L: $_SESSION['username'] = username
L->>L: setcookie("username", ..., 30 days)
L-->>B: 302 Location: welcome.php
B->>W: GET /welcome.php
W->>W: session_start() + timeout check + cookie fallback
W-->>B: 200 Welcome page
B->>O: GET /logout.php
O->>O: session_unset() + session_destroy()
O-->>B: 302 Location: login.php
The complete file set for this authentication system:
login.php— form, DB connection, password verification, session + cookie creation, redirectwelcome.php— session guard, timeout check, cookie fallback, protected contentlogout.php— session teardown, redirect
🧪 Try It Yourself
Task: Build the complete three-file authentication system locally.
- Create the
user_managementdatabase anduserstable using the SQL above. - Create
login.php,welcome.php, andlogout.phpwith the code from this lesson. - Start XAMPP (or your local PHP server) and navigate to
http://localhost/login.php. - Log in with username
adminand passwordpassword.
Success criterion: After login you should be redirected to welcome.php and see "Welcome, admin". Clicking Logout should return you to login.php. If you manually visit welcome.php without logging in first, you should be immediately redirected to login.php.
Starter insert to add a second test user (hash is for the password "test123"):
INSERT INTO users (username, password) VALUES
('testuser', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi');
🔍 Checkpoint Quiz
Q1. Why must session_start() be called before any HTML output is sent to the browser?
A) It is a performance optimisation
B) PHP sessions depend on a cookie header, and headers cannot be sent after body output begins
C) It initialises the MySQL connection
D) It is optional — you can call it anywhere
Q2. Given the following snippet, what happens if a user visits welcome.php 35 minutes after their last activity?
$timeout_duration = 1800;
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY']) > $timeout_duration) {
session_unset();
session_destroy();
header("Location: login.php");
exit();
}
$_SESSION['LAST_ACTIVITY'] = time();
A) The page loads normally because $_SESSION['LAST_ACTIVITY'] is updated
B) The session is destroyed and the user is redirected to login.php
C) A PHP warning is thrown because time() returns a float
D) Nothing — the timeout block only runs on login.php
Q3. What is the difference between session_unset() and session_destroy()? When should you use both together?
(Open-ended — write your answer before checking.)
Q4. You want a "remember me" cookie to persist for exactly 7 days. Which setcookie() call is correct?
A) setcookie("username", $u, 7, "/");
B) setcookie("username", $u, time() + 604800, "/");
C) setcookie("username", $u, time() + (86400 / 7), "/");
D) setcookie("username", $u, "7 days", "/");
A1. B — session_start() sends a Set-Cookie HTTP header to deliver the session ID to the browser. HTTP headers must be sent before the response body. Any echo, HTML, or whitespace before session_start() will trigger a "headers already sent" fatal error.
A2. B — 35 minutes exceeds the 1800-second (30-minute) threshold. The condition evaluates to true, session_unset() and session_destroy() run, and header("Location: login.php") redirects the user. The $_SESSION['LAST_ACTIVITY'] = time() line is never reached because exit() stops execution.
A3. session_unset() clears all variables stored inside $_SESSION (the in-memory array), but the session file on the server still exists. session_destroy() deletes that server-side file. Using only session_unset() leaves an empty session alive; using only session_destroy() without session_unset() may leave stale $_SESSION data accessible for the remainder of the current script. Always call both for a clean, complete logout.
A4. B — 86400 seconds × 7 = 604800 seconds. time() + 604800 sets the expiry to exactly 7 days from now. Option A passes 7 as a Unix timestamp (January 1, 1970), which is already expired. Option C divides instead of multiplying. Option D passes a string, which setcookie() does not accept for the expiry argument.
🪞 Recap
session_start()must be the first call on every page that uses sessions, before any output.password_verify()safely compares a plain-text password against a bcrypt hash — never compare passwords as plain strings.- Protect every restricted page with a session guard (
!isset($_SESSION['username'])→ redirect) plusexit(). - A session timeout using
$_SESSION['LAST_ACTIVITY']andtime()automatically logs out inactive users after a configurable window (e.g., 1800 seconds). setcookie("username", $username, time() + (86400 * 30), "/")persists the username for 30 days;$_COOKIE['username']reads it back.- Logout requires both
session_unset()(clear variables) andsession_destroy()(delete server-side file), followed byheader("Location: login.php")andexit().
📚 Further Reading
- PHP Manual: Sessions — the source of truth on
session_start,session_destroy, and session configuration - PHP Manual: setcookie() — full parameter reference for cookie creation and expiry
- PHP Manual: password_hash() and password_verify() — official documentation on bcrypt hashing in PHP
- OWASP Session Management Cheat Sheet — industry best-practice guide for secure session handling
- ⬅️ Previous: Joint Query, Nested Query, Filtering DATA
- ➡️ Next: Tags List