web php

Authorization Basics

A user may be logged in and still not be allowed to edit another user's post, view an admin page, refund an order, or download a private file.

Check permissions on the server

Hiding a button in HTML is useful for usability, but it is not security. The server must check permission when the protected action is requested.

PHP example
<?php

declare(strict_types=1);

/**
 * @param array{id: int, role: string} $user
 * @param array{id: int, author_id: int} $post
 */
function canEditPost(array $user, array $post): bool
{
    if ($user['role'] === 'admin') {
        return true;
    }

    return (int) $post['author_id'] === (int) $user['id'];
}

$currentUser = ['id' => 10, 'role' => 'author'];
$post = ['id' => 99, 'author_id' => 10];

if (!canEditPost($currentUser, $post)) {
    http_response_code(403);
    exit('Forbidden');
}

echo 'Edit allowed' . PHP_EOL;

// Prints:
// Edit allowed

This is an object-level permission check. The rule depends on both the user and the specific post.

Common authorization rules

Common rules include:

  • role-based access, such as admin or editor
  • ownership, such as a user editing their own post
  • team or tenant membership
  • feature flags or subscription level
  • record state, such as "only draft posts can be edited"

Real systems often combine several rules.

PHP example
<?php

declare(strict_types=1);

/**
 * @param array{id: int, role: string, team_id: int} $user
 * @param array{team_id: int, status: string} $document
 */
function canPublishDocument(array $user, array $document): bool
{
    if ($user['role'] !== 'editor' && $user['role'] !== 'admin') {
        return false;
    }

    if ($user['team_id'] !== $document['team_id']) {
        return false;
    }

    return $document['status'] === 'draft';
}

$user = ['id' => 5, 'role' => 'editor', 'team_id' => 2];
$document = ['team_id' => 2, 'status' => 'draft'];

echo canPublishDocument($user, $document) ? 'Can publish' : 'Cannot publish';

// Prints:
// Can publish

403 or 404

403 Forbidden means the user is authenticated but not allowed to do the action.

Sometimes applications return 404 Not Found instead to avoid revealing that a private record exists. For example, /invoices/123 might return 404 to users outside the tenant.

Choose deliberately and follow the project's convention.

Protect reads and writes

Developers often remember to protect edit and delete actions, then forget read actions. Viewing a private document, downloading a file, or listing another team's records also needs authorization.

Authorization should happen close to the action or query. For list pages, the database query itself should usually be scoped to records the user can see.

What to check in a project

Check that every protected route has an authorization path, not only a login check.

Check object-level access. can edit posts is weaker than can edit this post.

Check list queries. A user should not see records from another tenant or account because a query forgot a team_id filter.

Check hidden UI. Buttons can be hidden for convenience, but the server endpoint still needs enforcement.

Check tests or examples for both allowed and denied cases.

What you should be able to do

After this lesson, you should be able to distinguish authorization from authentication, write simple role and ownership checks, protect server actions, and recognise when to return 403 or hide a resource with 404.

Practice

Task: Authorize A Post Edit

Write a small PHP script that decides whether a user can edit a post.

Requirements

  • Use declare(strict_types=1);.
  • Allow admins to edit any post.
  • Allow authors to edit their own posts.
  • Deny authors editing someone else's post.
  • Include all three cases in the output.
  • Return a clear allowed or denied result.

Check Your Work

Run the script and confirm the denied case is not treated as a missing login.

Show solution

This solution checks both role and ownership.

PHP example
<?php

declare(strict_types=1);

/**
 * @param array{id: int, role: string} $user
 * @param array{id: int, author_id: int} $post
 */
function canEditPost(array $user, array $post): bool
{
    if ($user['role'] === 'admin') {
        return true;
    }

    return $post['author_id'] === $user['id'];
}

$post = ['id' => 99, 'author_id' => 10];
$cases = [
    'admin' => ['id' => 1, 'role' => 'admin'],
    'owner' => ['id' => 10, 'role' => 'author'],
    'other author' => ['id' => 11, 'role' => 'author'],
];

foreach ($cases as $label => $user) {
    echo $label . ': ' . (canEditPost($user, $post) ? 'allowed' : 'denied') . PHP_EOL;
}

// Prints:
// admin: allowed
// owner: allowed
// other author: denied

In a real endpoint, the denied branch would return a 403 Forbidden response or, for private resources, sometimes a deliberate 404 Not Found.

Why This Works

The examples prove the rule for privileged users, owners, and authenticated users who still lack permission.