Skip to content
Ray Fung edited this page Feb 26, 2026 · 3 revisions

CSRF Protection

Razy provides CSRF (Cross-Site Request Forgery) protection through a token manager and middleware. Tokens are stored in the session, validated with timing-safe comparison, and enforced automatically on state-changing HTTP methods.


Table of Contents


Quick Start

use Razy\Session\Session;

use Razy\Session\Driver\FileDriver;

use Razy\Csrf\CsrfTokenManager;

use Razy\Csrf\CsrfMiddleware;

use Razy\Distributor\MiddlewarePipeline;



// 1. Create session

$session = new Session(new FileDriver('/tmp/sessions'));



// 2. Create CSRF token manager

$csrf = new CsrfTokenManager($session);



// 3. Add CSRF middleware to pipeline

$pipeline = new MiddlewarePipeline();

$pipeline->pipe(new SessionMiddleware($session));

$pipeline->pipe(new CsrfMiddleware($csrf));



// 4. Get token for forms/AJAX

$token = $csrf->token();

CsrfTokenManager

Manages CSRF tokens via the session. Tokens are 64-character hex strings generated from random_bytes(32).

use Razy\Csrf\CsrfTokenManager;



$csrf = new CsrfTokenManager($session);



// Get or generate a token

$token = $csrf->token();

// 'a3f8e2b1c4d6...64 chars'



// Validate a submitted token (timing-safe via hash_equals)

$valid = $csrf->validate($_POST['_token']);  // bool



// Regenerate the token (e.g., after login)

$newToken = $csrf->regenerate();



// Check if a token exists

$csrf->hasToken();  // bool



// Remove the token from session

$csrf->clearToken();



// Access the underlying session

$csrf->getSession();

Session Storage

| Constant | Value | Description |

| --- | --- | --- |

| SESSION_KEY | '_csrf_token' | Key used in session storage |

| TOKEN_BYTES | 32 (private) | Random bytes (produces 64-char hex) |


CsrfMiddleware

The middleware validates tokens on state-changing requests (POST, PUT, PATCH, DELETE) and passes safe methods (GET, HEAD, OPTIONS) through without validation.

Configuration Options

use Razy\Csrf\CsrfMiddleware;



$middleware = new CsrfMiddleware(

    tokenManager: $csrf,



    // Custom mismatch handler (default: HTTP 419)

    onMismatch: function (array $context) {

        return ['error' => 'Invalid CSRF token', 'code' => 419];

    },



    // Routes to skip CSRF validation

    excludedRoutes: ['/api/webhook', '/api/stripe/hook'],



    // Rotate token after each successful validation

    rotateOnSuccess: false,



    // Custom token extraction logic

    tokenExtractor: function (array $context) {

        return $context['headers']['x-custom-token'] ?? null;

    },

);

Token Extraction Order

The middleware tries these sources in order:

  1. Custom $tokenExtractor closure (if provided)

  2. $_POST['_token'] — form field

  3. $_SERVER['HTTP_X_CSRF_TOKEN'] — HTTP header

  4. $context['_token'] — context key

The first non-null value is used for validation.

Excluding Routes

$middleware = new CsrfMiddleware(

    tokenManager: $csrf,

    excludedRoutes: [

        '/api/webhook',        // exact match against $context['route']

        '/api/stripe/hook',

    ],

);

Excluded routes bypass CSRF validation entirely, regardless of HTTP method.

Token Rotation

$middleware = new CsrfMiddleware(

    tokenManager: $csrf,

    rotateOnSuccess: true,  // generate new token after each valid POST

);

When enabled, $csrf->regenerate() is called after each successful validation. This provides one-time-use tokens for maximum security but requires the frontend to fetch a new token after each form submission.


Usage in Forms

PHP Template

<form method="POST" action="/profile/update">

    <input type="hidden" name="_token" value="<?= htmlspecialchars($csrf->token()) ?>">

    <input type="text" name="name" value="<?= htmlspecialchars($user['name']) ?>">

    <button type="submit">Save</button>

</form>

Razy Template Engine

<!-- In a Razy template -->

<form method="POST" action="/profile/update">

    <input type="hidden" name="_token" value="{$csrf_token}">

    <input type="text" name="name" value="{$user.name}">

    <button type="submit">Save</button>

</form>

Usage in AJAX

JavaScript (Fetch API)

// Get the token from a meta tag or hidden field

const token = document.querySelector('meta[name="csrf-token"]')?.content

    || document.querySelector('[name="_token"]')?.value;



// Include in requests

fetch('/api/data', {

    method: 'POST',

    headers: {

        'Content-Type': 'application/json',

        'X-CSRF-TOKEN': token,

    },

    body: JSON.stringify({ name: 'Alice' }),

});

HTML Meta Tag

<meta name="csrf-token" content="<?= htmlspecialchars($csrf->token()) ?>">

jQuery

$.ajaxSetup({

    headers: {

        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),

    },

});



$.post('/api/data', { name: 'Alice' });

TokenMismatchException

Thrown when CSRF validation fails (error code 419).

use Razy\Csrf\TokenMismatchException;



try {

    // Manual validation

    if (!$csrf->validate($submittedToken)) {

        throw new TokenMismatchException();

    }

} catch (TokenMismatchException $e) {

    echo $e->getMessage();  // 'CSRF token mismatch.'

    echo $e->getCode();     // 419

}

Custom message:

throw new TokenMismatchException('Your session has expired. Please refresh the page.');

API Reference

CsrfTokenManager

| Method | Signature | Returns |

| --- | --- | --- |

| __construct | (SessionInterface $session) | |

| token | (): string | Get/generate 64-char hex token |

| validate | (string $submittedToken): bool | Timing-safe comparison |

| regenerate | (): string | Replace and return new token |

| hasToken | (): bool | Token exists in session? |

| clearToken | (): void | Remove from session |

| getSession | (): SessionInterface | Underlying session |

CsrfMiddleware

| Method | Signature | Returns |

| --- | --- | --- |

| __construct | (CsrfTokenManager, ?Closure $onMismatch, array $excludedRoutes, bool $rotateOnSuccess, ?Closure $tokenExtractor) | |

| handle | (array $context, Closure $next): mixed | Validates or short-circuits |

| getTokenManager | (): CsrfTokenManager | Access token manager |

| Constant | Value | Description |

| --- | --- | --- |

| SAFE_METHODS | ['GET', 'HEAD', 'OPTIONS'] | Methods that bypass validation |

| TOKEN_FIELD | '_token' | POST field name |

| TOKEN_HEADER | 'X-CSRF-TOKEN' | HTTP header name |

TokenMismatchException

| Method | Signature |

| --- | --- |

| __construct | (string $message = 'CSRF token mismatch.') |

Extends \RuntimeException with error code 419.

← Previous: Session

ORM

Clone this wiki locally