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