Skip to content

PHP Tutorial Authentication

Protecting Pages

Lesson 287 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

A page that should be private must check on the server that the visitor is logged in, before it shows anything.

A require-login function

Put the check in one function, and call it at the top of every private page.

index.php
PHP
<?php
function requireLogin(array $session): int
{
    $userId = $session["user_id"] ?? null;

    if (!is_int($userId)) {
        // header("Location: login.php"); exit;   <- in a real page
        throw new RuntimeException("Login required");
    }

    return $userId;
}

try {
    requireLogin([]);
} catch (RuntimeException $e) {
    echo $e->getMessage();
}

echo " / ";
echo requireLogin(["user_id" => 7]);
Output
OUTPUT
Login required / 7

A real version redirects and stops the script.

auth.php
PHP
<?php
declare(strict_types=1);

function requireLogin(): int
{
    if (!isset($_SESSION["user_id"]) || !is_int($_SESSION["user_id"])) {
        header("Location: login.php");
        exit;   // IMPORTANT: stop the script, or the page content is still sent
    }

    return $_SESSION["user_id"];
}
dashboard.php
PHP
<?php
declare(strict_types=1);

session_start();

require __DIR__ . "/auth.php";

$userId = requireLogin();   // everything below runs only for logged-in users

Do not forget exit

header("Location: ...") only asks the browser to go elsewhere. The script keeps running unless you call exit, and the private content may still be sent in the response body.

Role checks

Different users may do different things. A simple way is a role column.

index.php
PHP
<?php
function hasRole(array $user, string ...$roles): bool
{
    return in_array($user["role"] ?? "", $roles, true);
}

$admin = ["id" => 1, "role" => "admin"];
$member = ["id" => 2, "role" => "user"];

var_dump(hasRole($admin, "admin"));
var_dump(hasRole($member, "admin"));
var_dump(hasRole($member, "admin", "user"));
Output
OUTPUT
bool(true)
bool(false)
bool(true)

On an admin page:

index.php
PHP
<?php
$user = currentUser($pdo, $_SESSION);

if ($user === null) {
    header("Location: login.php");
    exit;
}

if (!hasRole($user, "admin")) {
    http_response_code(403);
    exit("You do not have access to this page.");
}

401 means “not logged in”, 403 means “logged in, but not allowed”. A redirect to the login page is the friendlier answer to a missing login.

Ownership checks

Roles are not enough. A normal user may edit their own posts but not other people’s. Check the owner in the SQL or in the code.

index.php
PHP
<?php
function canEdit(array $user, array $post): bool
{
    return $user["role"] === "admin" || (int) $post["user_id"] === (int) $user["id"];
}

$post = ["id" => 10, "user_id" => 2];

var_dump(canEdit(["id" => 2, "role" => "user"], $post));
var_dump(canEdit(["id" => 3, "role" => "user"], $post));
var_dump(canEdit(["id" => 1, "role" => "admin"], $post));
Output
OUTPUT
bool(true)
bool(false)
bool(true)

Without it, a user can change a number in the URL (post-edit.php?id=11) and edit someone else’s post. This weakness is called IDOR (insecure direct object reference). It is one of the most common real-world bugs.

Hiding the “Admin” menu item from normal users is good design. It is not security. Anyone can type the address. Every protected page and action must check on the server.

Protect actions, not only pages

A POST handler (delete, update) needs the same checks: logged in, allowed, valid CSRF token. Do not assume that “the form is only shown to admins”.

Pages in one folder

If a whole folder is private (/admin/), include the check in a shared file at the top of every page there, for example admin/bootstrap.php. If you forget one file, that page is open.

dangerous
Check, then output
Run the access check before any output and before any database query that reveals data. A page that checks at the bottom has already leaked the content.