Sessions & CSRF¶
Note
Not part of core. Install it separately:
composer require kinetis/session
Cookie-backed sessions and CSRF protection for browser applications — the counterpart to the token-based Authentication and JWT Authentication packages. One store interface, three storage drivers, and two route middlewares.
Configuration¶
Installing the package is the only setup step; SESSION_DRIVER turns
it on:
Key |
Default |
Purpose |
|---|---|---|
|
— |
|
|
|
Seconds a session stays readable, counted from its last write. |
|
|
The cookie name. A |
|
|
The cookie’s |
|
|
The cookie’s |
|
system temp |
The |
The three drivers:
file— one JSON file per session, no backing service; suited to local development. An expired file is deleted the next time it is read; files for sessions never touched again stay untilsession:gcsweeps them (see below).cache— sessions through the PSR-16CacheInterfacebinding. WithREDIS_HOST/REDIS_URLconfigured andkinetis/cache-redisinstalled, that means Redis-backed sessions with zero further code — cluster mode and TLS included, since the same binding already provides both. No garbage collection is needed with this driver: the backend expires entries itself — a Redis session key simply disappears when its TTL lapses. ANullSimpleCachebinding is rejected at construction: a session store that never stores would mean logins that silently don’t stick.sql— akinetis_sessionstable, using the database connectionDB_CONNECTIONprovides. The table is not created automatically; it ships as ready-to-copy Migrations stubs:vendor/kinetis/session/resources/migrations/create_kinetis_sessions_table.mysql.php.stub vendor/kinetis/session/resources/migrations/create_kinetis_sessions_table.pgsql.php.stub
An expired session is invisible to reads but its row stays in the table until
session:gcdeletes it (see below).
Garbage collection¶
The file and sql drivers keep expired sessions in storage until
something deletes them. The session:gc command is that something:
php vendor/bin/kinetis session:gc
It deletes every expired session from whichever store is bound and prints how many were removed. Nothing runs it implicitly — schedule it with whatever the deployment already uses (cron, a Kubernetes CronJob, an EventBridge rule), the same way any other CLI command is scheduled. Once a day is plenty for most applications; expired sessions are already invisible to reads either way, so the schedule only controls how long dead data lingers, never correctness.
With the cache driver there is nothing to schedule: the backend
expires entries on its own (Redis drops a session key the moment its
TTL lapses), and session:gc says so and exits 0. A custom store
joins the command by implementing
GarbageCollectableStoreInterface — one method, gc(): int.
Using the session¶
SessionMiddleware is route middleware, never global — it
registers the request’s Session on the request’s own scope, and only
route middleware resolves through that scope. Attach it to a controller
and inject Session:
use Kinetis\Http\Attributes\Get;
use Kinetis\Http\Attributes\Middleware;
use Kinetis\Session\Middleware\SessionMiddleware;
use Kinetis\Session\Session;
#[Middleware(SessionMiddleware::class)]
final readonly class PreferencesController
{
public function __construct(private Session $session) {}
#[Get('/theme/{name}')]
public function choose(string $name): array
{
$this->session->set('theme', $name);
return ['theme' => $name];
}
#[Get('/theme')]
public function current(): array
{
return ['theme' => $this->session->get('theme', 'light')];
}
}
get()/set()/has()/remove()/all() are the surface;
flash($key, $value) stores a value that survives exactly one
following request, read back with flashed($key) — the classic
post-redirect-get companion. Values must be JSON-serializable: stores
encode with JSON, never PHP’s native serialize(), so a crafted
payload can never become an object-injection vector.
Loading is lazy and persisting is conditional: a route that never
touches its session performs no storage round trip and sends no
Set-Cookie, so attaching the middleware broadly costs nothing on
session-free requests. The cookie is always HttpOnly; Secure and
SameSite come from configuration.
Call regenerate() whenever privilege changes — especially on
login. It gives the session a fresh id, keeps its data, and destroys
the old id’s payload, so a session id an attacker planted before login
stops working. destroy() is logout: payload gone, cookie expired.
CSRF protection¶
CsrfMiddleware enforces a synchronizer token on state-changing
methods — GET/HEAD/OPTIONS pass untouched. Stack it after
SessionMiddleware (declaration order is execution order):
#[Middleware(SessionMiddleware::class)]
#[Middleware(CsrfMiddleware::class)]
final readonly class OrderController
{
// ...
}
The token comes from Session::csrfToken() — render it into a form’s
_token field or hand it to a client that then sends the
X-CSRF-Token header. A missing or mismatched token is a 403;
comparison uses hash_equals(), so it is not vulnerable to timing
attacks.
JSON requests use the header: Kinetis decodes JSON bodies inside the
dispatcher, so a _token field inside a JSON body is not seen by this
middleware — only form-encoded bodies carry _token.
Concurrency: last-write-wins¶
No store locks. PHP’s native session handler locks the session file, serializing a browser’s parallel requests against each other; that would conflict with the concurrent-worker model the whole framework is built around. Concurrent requests sharing one session are last-write-wins — which is why session data should stay small and low-contention (an auth reference, the CSRF token, flash data), not a shared mutable workspace.
Custom stores¶
SessionStoreInterface is three methods — read, write, destroy —
and anything implementing it can be bound in bootstrap.php to replace
what SESSION_DRIVER would have picked:
$app->bind(SessionStoreInterface::class, static fn (): MyStore => new MyStore(...));
See also¶
Authentication / JWT Authentication — token-based authentication, the API-first counterpart to cookie sessions.
Persistence — the SQL contracts the
sqldriver builds on.Middleware — how route middleware and middleware groups work.