Built-in Middleware
Understanding Auth, Guest, and CSRF middleware implementations
The Three Guards
DALT.PHP comes with three essential middleware:
- Auth - Must be logged in
- Guest - Must NOT be logged in
- CSRF - Must have valid form token
Let's understand each one in detail.
Auth Middleware: Login Required
// framework/Core/Middleware/Auth.php
public function handle()
{
if(!($_SESSION['user'] ?? false)){
header('location: /' );
exit();
}
}What It Does
Checks if $_SESSION['user'] exists. If not, redirect to /.
Breaking It Down
The check:
$_SESSION['user'] ?? falseThis means:
- If
$_SESSION['user']exists → use its value - If it doesn't exist → use
false
The condition:
if(!(...)){The ! means "not", so:
- If user exists → condition is false → continue
- If user doesn't exist → condition is true → redirect
When to Use
$router->get('/dashboard', 'dashboard/index.php')->only('auth');
$router->get('/profile', 'profile/index.php')->only('auth');
$router->post('/posts', 'posts/store.php')->only('auth');Any route that requires a logged-in user.
How Users Get Logged In
The Authenticator class sets this session:
// After successful login
$_SESSION['user'] = [
'email' => $email,
];So Auth middleware just checks if that was set.
Guest Middleware: Must Be Logged Out
// framework/Core/Middleware/Guest.php
public function handle()
{
if($_SESSION['user'] ?? false){
header('location: /');
exit();
}
}What It Does
The opposite of Auth - checks if user is logged in, and if so, redirects away.
Why This Exists
Prevents logged-in users from seeing login/register pages:
$router->get('/login', 'session/create.php')->only('guest');
$router->get('/register', 'registration/create.php')->only('guest');Without Guest middleware:
- Logged-in user visits
/login - Sees login form (confusing!)
- Could try to login again (weird)
With Guest middleware:
- Logged-in user visits
/login - Redirected to
/(home page) - Better UX
The Logic
if($_SESSION['user'] ?? false){
// User IS logged in
// This is a guest-only page
// Redirect them away
}CSRF Middleware: Form Protection
CSRF (Cross-Site Request Forgery) is a serious security issue. This middleware prevents it.
What is CSRF?
Imagine:
- You're logged into
yourbank.com - You visit
evil.com evil.comhas a hidden form:<form action="https://yourbank.com/transfer" method="POST"> <input name="to" value="attacker"> <input name="amount" value="1000"> </form> <script>document.forms[0].submit();</script>- Your browser sends the request WITH your bank cookies
- Money transferred without your knowledge!
How CSRF Tokens Prevent This
Step 1: Server generates a random token
$_SESSION['_csrf'] = bin2hex(random_bytes(32));Step 2: Form includes the token
<form method="POST" action="/transfer">
<input type="hidden" name="_token" value="abc123xyz...">
<!-- other fields -->
</form>Step 3: Server checks the token
if ($_POST['_token'] !== $_SESSION['_csrf']) {
die('CSRF attack detected!');
}Why this works:
evil.comdoesn't know your CSRF token- It's stored in your session (server-side)
- The attacker can't read it
- So the forged form fails the check
CSRF Middleware Implementation
public function handle(): void
{
$method = $_POST['_method'] ?? $_SERVER['REQUEST_METHOD'];
$method = strtoupper($method);
if ($method === 'GET') {
return;
}
$sessionToken = $_SESSION['_csrf'] ?? null;
$formToken = $_POST['_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;
if (!$sessionToken || !$formToken || !hash_equals($sessionToken, $formToken)) {
http_response_code(419);
echo 'CSRF token mismatch';
exit;
}
}Step-by-Step Breakdown
1. Get the HTTP method
$method = $_POST['_method'] ?? $_SERVER['REQUEST_METHOD'];
$method = strtoupper($method);Why check $_POST['_method']?
- HTML forms only support GET and POST
- For PUT/PATCH/DELETE, we use method spoofing:
<input type="hidden" name="_method" value="DELETE">
2. Skip GET requests
if ($method === 'GET') {
return;
}GET requests should be safe (read-only), so no CSRF check needed.
3. Get tokens
$sessionToken = $_SESSION['_csrf'] ?? null;
$formToken = $_POST['_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;Two ways to send the token:
- Form field:
<input name="_token"> - HTTP header:
X-CSRF-TOKEN(for AJAX)
4. Validate
if (!$sessionToken || !$formToken || !hash_equals($sessionToken, $formToken)) {
http_response_code(419);
echo 'CSRF token mismatch';
exit;
}Fails if:
- No session token (session expired?)
- No form token (forgot to include it?)
- Tokens don't match (CSRF attack!)
Why hash_equals()?
You might wonder: why not just use ===?
// DON'T DO THIS
if ($sessionToken === $formToken) {The Timing Attack Problem
String comparison in PHP stops at the first different character:
Token 1: "abc123"
Token 2: "xyz789"
↑ Different at position 0
Comparison time: 1 microsecond
Token 1: "abc123"
Token 2: "abc789"
↑ Different at position 3
Comparison time: 3 microsecondsAn attacker can measure response times:
- Try "a..." → fast fail
- Try "b..." → fast fail
- Try "c..." → slower fail (first char matches!)
- Keep guessing character by character
The Solution: hash_equals()
hash_equals($sessionToken, $formToken)This function:
- Always takes the same time
- Compares every character
- Prevents timing attacks
It's a small detail, but important for security.
Using CSRF in Forms
The Helper Function
function csrf_field()
{
$token = $_SESSION['_csrf'] ?? '';
return '<input type="hidden" name="_token" value="' . htmlspecialchars($token) . '">';
}In Your Views
<form method="POST" action="/posts">
<?= csrf_field() ?>
<input name="title" type="text">
<button type="submit">Create</button>
</form>For AJAX Requests
fetch('/api/posts', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({title: 'Hello'})
});Middleware Comparison
| Middleware | Checks | Redirects | Use Case |
|---|---|---|---|
| Auth | User logged in? | → / if not | Protected pages |
| Guest | User logged out? | → / if logged in | Login/register |
| CSRF | Valid token? | 419 error if not | All POST/PUT/DELETE |
Common Patterns
Public Page
$router->get('/', 'welcome.php');
// No middlewareProtected Page
$router->get('/dashboard', 'dashboard/index.php')->only('auth');Login Page
$router->get('/login', 'session/create.php')->only('guest');Form Submission
$router->post('/posts', 'posts/store.php')->only(['auth', 'csrf']);API Endpoint (No CSRF)
$router->post('/api/posts', 'api/posts/store.php')->only('auth');
// CSRF not needed for API (uses tokens instead)Key Takeaways
- Auth checks session - Simple existence check
- Guest is inverse of Auth - Prevents logged-in access
- CSRF prevents attacks - Validates form tokens
- hash_equals() is important - Prevents timing attacks
- Middleware order matters - Auth before CSRF
What's Good Here
✅ Simple implementations (easy to understand)
✅ CSRF uses timing-safe comparison
✅ Supports both form and header tokens
✅ Clear separation of concerns
✅ Auth middleware redirects to /login (proper UX)
Design Note
Features like "remember me", CSRF rotation, and rate limiting are intentionally omitted for simplicity. These are important for production but would add complexity that obscures the core concepts DALT is teaching.
Part 2 Complete!
You now understand routing and middleware:
- How routes are registered and matched
- How the router works internally
- How middleware filters requests
- How Auth, Guest, and CSRF protect your app