-
Notifications
You must be signed in to change notification settings - Fork 0
CSRF
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.
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();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();| Constant | Value | Description |
| --- | --- | --- |
| SESSION_KEY | '_csrf_token' | Key used in session storage |
| TOKEN_BYTES | 32 (private) | Random bytes (produces 64-char hex) |
The middleware validates tokens on state-changing requests (POST, PUT, PATCH, DELETE) and passes safe methods (GET, HEAD, OPTIONS) through without validation.
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;
},
);The middleware tries these sources in order:
-
Custom
$tokenExtractorclosure (if provided) -
$_POST['_token']— form field -
$_SERVER['HTTP_X_CSRF_TOKEN']— HTTP header -
$context['_token']— context key
The first non-null value is used for validation.
$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.
$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.
<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><!-- 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>// 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' }),
});<meta name="csrf-token" content="<?= htmlspecialchars($csrf->token()) ?>">$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
},
});
$.post('/api/data', { name: 'Alice' });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.');| 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 |
| 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 |
| Method | Signature |
| --- | --- |
| __construct | (string $message = 'CSRF token mismatch.') |
Extends \RuntimeException with error code 419.