Sessions, cookies and CSRF
daravel_session gives web routes a session the way Laravel does: the
session id travels in an encrypted cookie, the attributes live in a file or
in Laravel's sessions table as PHP serialize() text or JSON, and
state-changing requests must carry the CSRF token. A Laravel application and a Daravel
application with the same APP_KEY and the same store share a visitor's
session, cookies and token.
dart pub add daravel_session
// bootstrap/app.dart
import 'package:daravel_session/daravel_session.dart';
Application createApp() => Application.configure(basePath: basePath)
.withSession()
.withMigrations([...migrations, const CreateSessionsTable()])
.withRouting(
web: (Router router) => router.group((Router r) {
r.get('/cart', (request) => request.session.get<List<Object?>>('cart'));
}, middleware: const ['web']),
)
.create();
withSession() registers three middleware aliases and the web group that
runs them in Laravel's order:
| Alias | Middleware | Laravel |
|---|---|---|
encrypt_cookies |
EncryptCookiesMiddleware |
EncryptCookies |
session |
StartSessionMiddleware |
StartSession |
csrf |
VerifyCsrfTokenMiddleware |
VerifyCsrfToken |
Put the group on the routes that need a session. API routes with bearer tokens or JWTs do not need it, and do not pay for it.
Settings#
| Variable | Default | Purpose |
|---|---|---|
SESSION_DRIVER |
database |
database (the sessions table, as laravel/laravel 11 to 13), file (storage/framework/sessions) or array (tests). 1.18 defaulted to file: set SESSION_DRIVER=file to keep it |
SESSION_LIFETIME |
120 |
Minutes a session may stay idle |
SESSION_EXPIRE_ON_CLOSE |
false |
Session cookie without an expiry |
SESSION_COOKIE |
<app name>_session |
Cookie name, Str::slug(APP_NAME, '_') as Laravel derives it (my_app_session) |
SESSION_TABLE, SESSION_CONNECTION |
sessions, default |
The database driver's table and connection |
SESSION_PATH, SESSION_DOMAIN |
/, none |
Cookie scope |
SESSION_SECURE_COOKIE, SESSION_HTTP_ONLY, SESSION_SAME_SITE |
false, true, lax |
Cookie flags; SESSION_SAME_SITE=none needs the secure flag |
SESSION_ENCRYPT |
false |
Store the payload encrypted with Crypt, as Laravel's EncryptedStore (Crypt::encrypt(serialize($attributes))) |
SESSION_PARTITIONED_COOKIE |
false |
Partitioned (CHIPS) on the session and XSRF-TOKEN cookies, for an application embedded in another site |
SESSION_SERIALIZATION |
php, json with COMPAT_LARAVEL_VERSION=13 |
Format of new sessions, Laravel's session.serialization |
Both formats are always read, and a stored session is written back in the
format it has, so Daravel never logs anyone out of a Laravel application
that shares the store. SESSION_SERIALIZATION decides for sessions Daravel
creates, and Laravel must be able to read those: only the laravel/laravel 13
skeleton sets 'serialization' => 'json', so an application upgraded to 13
may still be on php. dartisan adopt reads the value from the Laravel
project's config/session.php.
With several isolates or servers use database (or the file driver on
one machine): the array driver is one isolate's memory. CreateSessionsTable
creates Laravel's table and does nothing when it exists already.
The session#
final session = request.session; // or Session.current
session.put('cart.items', ['a']); // dot keys nest, as in Laravel
session.push('cart.items', 'b');
session.get<String>('name', 'guest');
session.pull<int>('hits');
session.increment('views');
session.forget('name');
session.flush();
session.flash('status', 'Saved.'); // next request only
session.now('status', 'Saving...'); // this request only
session.reflash();
session.keep(['status']);
session.token; // the CSRF token
await session.regenerate(); // new id, new token, same data
await session.invalidate(); // flush + new id (logout)
session.previousUrl; // the last GET page
The attributes are written back after the response as serialize() text,
with Laravel's keys for the framework's own entries (_token, _flash.old,
_flash.new, _previous.url), so a Laravel application reading the same
file or row sees what it would have written. A PHP object Laravel stored
(a ViewErrorBag, for example) arrives as a PhpObject and is written
back unchanged.
Cookies#
EncryptCookiesMiddleware encrypts every cookie the response sets and
decrypts the ones the request carries, with Crypt and Laravel's value
prefix (hmac_sha1(name . 'v2', key) . '|'), so a cookie cannot be moved
to another name and a Laravel application reads the same cookies. A cookie
that fails to decrypt is dropped from the request. Cookies that must stay
readable by scripts or other systems go in withSession(cookiesExcept: ['theme']).
CSRF#
VerifyCsrfTokenMiddleware lets GET, HEAD and OPTIONS through and
requires the session's token on anything else, from the _token input, the
X-CSRF-TOKEN header or the X-XSRF-TOKEN header. Every response sets the
XSRF-TOKEN cookie (encrypted, readable by scripts), which Axios and
Angular send back as X-XSRF-TOKEN. A missing or wrong token answers 419
with {"message": "CSRF token mismatch."}, as Laravel does. Webhooks and
other endpoints called without a session skip the check with
withSession(csrfExcept: ['webhooks/*']).
<form method="post" action="/profile">
<input type="hidden" name="_token" value="{{ request.session.token }}">
</form>
Logging in#
SessionGuard is Laravel's session guard. It keeps the user id under the
same login_web_<sha1> key, so a user logged in on either application is
logged in on both.
.withAuth((auth) => auth.session<User>('web', ModelUserProvider<User>(User.definition)))
final guard = App.make<AuthManager>().guard('web') as SessionGuard<User>;
await guard.attempt({'email': email, 'password': password}); // false on a mismatch
await guard.login(user); // new session id and token
await guard.logout();
await request.session.invalidate();
attempt(..., remember: true) also writes Laravel's remember_web_<sha1>
cookie, which needs a nullable remember_token column and logs the user in
on either application once the session is gone; logout forgets it and
changes the token. guard.viaRemember tells whether this
request's user came from that cookie.
A failed validation in the web group sends a browser or an Inertia visit
back, 303 after an Inertia PUT, PATCH or DELETE, with session.errors and
session.old('name') on the next request; passwords are never flashed. Clients
expecting JSON keep the 422. Forms name their verb with _method, or the
X-HTTP-Method-Override header does; middleware.methodOverride = false turns
this off.
Put auth:web after the web group on protected routes. From
COMPAT_LARAVEL_VERSION=12 the login also stores password_hash_web, which
Laravel's AuthenticateSession compares.
Not covered#
The cookie session driver, ViewErrorBag (back()->withErrors()),
and session blocking (Route::block). Laravel's sessions table and cookie formats are the contract; the
rest is Daravel's own API.
Laravel equivalents#
More of Laravel that this works with, and the API to look up on pub.dev.
| Laravel | Daravel | API | Note |
|---|---|---|---|
Route::middleware('web') |
.group(routes, middleware: ['web']) |
SessionApplicationBuilder |
withSession() registers encrypt_cookies, session and csrf, and the web group that runs them in Laravel's order. |
SESSION_DRIVER=file|database |
SESSION_DRIVER=file|database (array for tests) |
SessionManager.extend |
Laravel's sessions table (payload base64(serialize()) or base64(json_encode()), last_activity) and file format; extend() adds a driver. |