Authentication answers "who are you?" (login). Authorisation answers "what are you allowed to do?" (privileges). Every request to a protected page must be checked on the server; hiding a button is not security.
Role-based access control (RBAC): each user has a role (customer, staff, admin); each page or action needs a minimum role.
| Action | Guest | Customer | Staff | Admin |
|---|---|---|---|---|
| Browse and search products | Yes | Yes | Yes | Yes |
| Add to cart | Yes | Yes | Yes | Yes |
| Checkout, view own orders | No | Yes | Yes | Yes |
| Add or edit products | No | No | Yes | Yes |
| Change order status | No | No | Yes | Yes |
| Manage users and roles | No | No | No | Yes |
includes/auth.php:
<?php
if (session_status() === PHP_SESSION_NONE) session_start();
function is_logged_in(): bool { return isset($_SESSION['user_id']); }
function current_role(): string { return $_SESSION['role'] ?? 'guest'; }
function require_login(): void {
if (!is_logged_in()) {
header('Location: /login.php?next=' . urlencode($_SERVER['REQUEST_URI']));
exit;
}
}
// Roles ranked by power
function role_level(string $role): int {
return ['guest' => 0, 'customer' => 1, 'staff' => 2, 'admin' => 3][$role] ?? 0;
}
function require_role(string $minRole): void {
require_login();
if (role_level(current_role()) < role_level($minRole)) {
http_response_code(403);
exit('403 Forbidden: you do not have permission.');
}
}Use at the very top of each protected page:
<?php
require '../includes/auth.php';
require_role('staff'); // admin/products.phpShow only what the user may use (UX only, not security):
<?php if (role_level(current_role()) >= 2): ?>
<a href='admin/products.php'>Manage products</a>
<?php endif; ?>Ownership checks (prevent "IDOR"). A customer must see only their own orders even if they edit the URL to order.php?id=99:
if (current_role() === 'admin') { // admins may open any order
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([$_GET['id'] ?? 0]);
} else { // customers only their own
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ? AND user_id = ?');
$stmt->execute([$_GET['id'] ?? 0, $_SESSION['user_id']]);
}
$order = $stmt->fetch();
if (!$order) { http_response_code(404); exit('Order not found'); }CSRF protection. Cross-site request forgery tricks a logged-in user's browser into sending a request they did not intend. Defend every state-changing form with a secret token:
<?php
// 1. When showing the form: create one secret token per session
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
?>
<form method='post' action='delete_product.php'>
<input type='hidden' name='csrf' value='<?= $_SESSION['csrf'] ?>'>
<input type='hidden' name='id' value='7'>
<button>Delete product</button>
</form>
<?php
// 2. When handling the POST: reject any request without the right token
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
http_response_code(403);
exit('Invalid request');
}Principle of least privilege: give each user and each database account only what they need. The shop's database user should not be able to drop tables.