Topic 35 of 48 · Full Stack Essentials

Sessions, login page, logout (Session destroy, timeout, Cookies introduction, Page redirect)

Lesson TL;DRTopic 7: Sessions, login page, logout (Session destroy, timeout, Cookies introduction, Page redirect) 📖 13 min read · 🎯 advanced · 🧭 Prerequisites: javascriptcookies, recreatetaskexternalcss Why th...
13 min read·advanced·php · sessions · authentication · cookies

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 users table that stores bcrypt-hashed passwords
  • Build a login.php page 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:

  • id is an auto-incrementing primary key.
  • username has a UNIQUE constraint — duplicate usernames are rejected at the database level.
  • password stores 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 with password_hash() and verify with password_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:

LinePurpose
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'] = $usernameStores 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 $_SESSION superglobal 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:

  1. On each page load, compare the current Unix timestamp (time()) against $_SESSION['LAST_ACTIVITY'].
  2. If the gap exceeds $timeout_duration (1800 seconds = 30 minutes), destroy the session and redirect.
  3. 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:

ArgumentValueMeaning
Name"username"Cookie name, read back via $_COOKIE['username']
Value$usernameThe value stored in the browser
Expirytime() + (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, redirect
  • welcome.php — session guard, timeout check, cookie fallback, protected content
  • logout.php — session teardown, redirect

🧪 Try It Yourself

Task: Build the complete three-file authentication system locally.

  1. Create the user_management database and users table using the SQL above.
  2. Create login.php, welcome.php, and logout.php with the code from this lesson.
  3. Start XAMPP (or your local PHP server) and navigate to http://localhost/login.php.
  4. Log in with username admin and password password.

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) plus exit().
  • A session timeout using $_SESSION['LAST_ACTIVITY'] and time() 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) and session_destroy() (delete server-side file), followed by header("Location: login.php") and exit().

📚 Further Reading

Like this topic? It’s one of 48 in Full Stack Essentials.

Block your seat for ₹2,500 and join the next cohort.