Tangible Bytes

A Web Developer’s Blog

Laravel Without Cookies

I used Drupal for years and recently I’m using Laravel for a project.

There is a lot to like about Laravel - but one thing that seems odd to me is the cookie handling.

Background

Drupal doesn’t set cookies for anonymous users (though you have to be careful to never start a session for these users) the great thing about this is that pages are more cacheable without cookies and it also helps comply with GDPR.

On the other hand Laravel by default sets both a session cookie and an XSRF token for every page request.

There are good reasons for using these for some sites (or parts of sites) but they can impact on performance and users have a right to reject all but strictly necessary ones.

gdpr.eu/cookies

Strictly necessary cookies — These cookies are essential for you to browse the website and use its features, such as accessing secure areas of the site. Cookies that allow web shops to hold your items in your cart while you are shopping online are an example of strictly necessary cookies. These cookies will generally be first-party session cookies. While it is not required to obtain consent for these cookies, what they do and why they are necessary should be explained to the user.

So : I’d rather not set cookies if I don’t have to but it seems those set by Laravel are acceptable in terms of the GDPR - we just need a policy document explaing why they are required and what they are used for.

How to Stop Laravel Setting Cookies

The default behavior is to set cookies and we can live with it - but do we have to ?

This post is useful : Cookieless Laravel (disable the use of cookies)

I’ll just summarise what it says - and then expand on what happens if you follow it.

Laravel Cookies are set by “middleware” and we can change this

Edit app/Http/Kernel.php moving the cookie related code out of the default web group to a new one called ‘cookies’

 /**
     * The application's route middleware groups.
     *
     * @var array<string, array<int, class-string|string>>
     */
    protected $middlewareGroups = [
        'web' => [
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
        'cookies' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
        ],
    ];

The either add this to routes/web.php

Route::middleware('cookies')->group(function () {
    Route::get('login', [LoginController::class, 'form']);
    Route::post('login', [LoginController::class, 'login']);
});

Or manually add the middleware in the boot() function of app/Providers/AppServiceProvider.php

    if (request()->is('login')) {
        $this->app['router']->pushMiddlewareToGroup('web', \Illuminate\Session\Middleware\StartSession::class);
        $this->app['router']->pushMiddlewareToGroup('web', \Illuminate\View\Middleware\ShareErrorsFromSession::class);
        $this->app['router']->pushMiddlewareToGroup('web', \App\Http\Middleware\VerifyCsrfToken::class);
    }

I found the first method worked when I created a site from scratch but I had to use the second on my existing project which used a package to manage auth.

What Happens

On the routes where to cookie middleware is set everything is Laravel-normal (cookies are set, CRSF protection is in place, messages set through the session work - etc)

On the routes where the cookie middleware is not set there are no cookies set CRSF protecion is not in place etc .

But also session cookies are not read on the the server either.

So even if you are already logged in - Laravel will not see the login for these routes.

A common pattern is to have a menu that changes when you login (even if it just the login/logout bit).

For pages like this Laravel will act as if you are logged out on those pages.

Thoughts

Getting the balance right here can be hard - and caching anonymous pages can be a huge performance boost but caches can be hard to manage well.

I suspect the best way to deal with this will be something like a varnish rule which strips these cookies from a whitelist of GET requests only when the user doesn’t already have a session cookie.