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