| Home | Getting Started | Core Concepts | Helpers | Extensions | Repo |
The CSRF extension generates and validates session-bound tokens for non-idempotent requests. Tokens are 32 bytes of cryptographic randomness, stored in $_SESSION, and consumed on first valid use (single-use by default).
It pairs naturally with $request->isValidCSRF() and $response->hasCSRFError() on the request/response objects — most of the time you’ll never call tiny::csrf() directly.
$csrf = tiny::csrf();
$csrf->generate(); // create + store + return a new token
$csrf->isValid($token = null, $remove = true); // validate; consumes by default
$csrf->input($echo = true); // render a hidden <input> for forms
$csrf->showError($id = 'CSRF-VALIDATION-FAILED', $nextPage = false);
$csrf->getTokenName(); // 'csrf_token'
isValid() reads the submitted token in this order: explicit argument → $_POST['csrf_token'] → $_GET['csrf_token'].
<form method="POST" action="/users">
<?php tiny::csrf()->input(); ?>
<input name="name" required>
<button type="submit">Create</button>
</form>
input() calls generate() lazily — if no token exists for this request, one is created. It emits:
<input type="hidden" name="csrf_token" value="…">
The shortest valid flow:
class Users extends TinyController
{
public function post($request, $response)
{
if (!$request->isValidCSRF()) {
$response->hasCSRFError(nextPage: true);
return $response->redirect('/users/new');
}
// ... create user
}
}
$request->isValidCSRF() is just a thin wrapper around tiny::csrf()->isValid() that also strips the token from the body.
$response->hasCSRFError() is the matching helper on the response object. It either sets tiny::data()->CSRFError (current page) or fires a flash toast for the next page load (nextPage: true).
HTMX form submissions naturally include the hidden input. For pure-JS clients, send the token in a header and read it from the rendered page:
<meta name="csrf-token" content="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
const token = document.querySelector('meta[name=csrf-token]').content;
fetch('/api/widgets', {
method: 'POST',
headers: { 'X-CSRF-Token': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Foo' }),
});
Then in the controller:
$token = $request->headers['X-CSRF-Token'] ?? null;
if (!tiny::csrf()->isValid($token)) {
return $response->sendJSON(['error' => 'Invalid CSRF token'], 403);
}
By default isValid() consumes the token. This is safer (replay-proof) but means SPAs need to fetch a fresh one between submissions. Pass remove: false to keep the token alive:
tiny::csrf()->isValid($token, remove: false);
A typical pattern is to expose a /api/csrf endpoint that returns a freshly generated token after each form submit.
$request->isValidCSRF() for POST/PUT/PATCH/DELETE that mutate state.X-CSRF-Token header.SameSite=Lax (or Strict) cookies for defense in depth.