DALT.PHP
Framework Deep Dive2. Routing

Built-in Middleware

Understanding Auth, Guest, and CSRF middleware implementations

The Three Guards

DALT.PHP comes with three essential middleware:

  1. Auth - Must be logged in
  2. Guest - Must NOT be logged in
  3. 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'] ?? false

This 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:

  1. You're logged into yourbank.com
  2. You visit evil.com
  3. evil.com has 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>
  4. Your browser sends the request WITH your bank cookies
  5. 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.com doesn'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 microseconds

An 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

MiddlewareChecksRedirectsUse Case
AuthUser logged in?/ if notProtected pages
GuestUser logged out?/ if logged inLogin/register
CSRFValid token?419 error if notAll POST/PUT/DELETE

Common Patterns

Public Page

$router->get('/', 'welcome.php');
// No middleware

Protected 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

  1. Auth checks session - Simple existence check
  2. Guest is inverse of Auth - Prevents logged-in access
  3. CSRF prevents attacks - Validates form tokens
  4. hash_equals() is important - Prevents timing attacks
  5. 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

On this page