Laravel - separate session lifetime for each user

490 views Asked by At

In my PHP Laravel application, each user (User model) belongs to an organization (Organization model). I need to be able to configure the session lifetime (session.lifetime config value) separately for each user based on the organization.

The value is stored in the database -> organizations table -> session_lifetime column (integer). Organization's admin users are able to change it in the admin page.

I have created a CustomSessionLifetime middleware:

public function handle(Request $request, Closure $next)
{
    $user = auth()->user();

    if ($user !== null)
    {
        $lifetime = $user->organization->session_lifetime;
        config()->set('session.lifetime', $lifetime);
    }
    
    return $next($request);
}

Now I have kind of a circular problem:

1) If I set the CustomSessionLifetime middleware to run BEFORE StartSession middleware, like this:

/**
 * The application's route middleware groups.
 *
 * @var array
 */
protected $middlewareGroups = [
    'web' => [
        //...
        \App\Http\Middleware\CustomSessionLifetime::class,
        \Illuminate\Session\Middleware\StartSession::class,
        //...
    ],

then I am unable to get the user (and thus the organization or the session_lifetime attribute value) using auth()->user() function because the session is not yet started.

2) If I set the CustomSessionLifetime middleware to run AFTER StartSession middleware, like this:

/**
 * The application's route middleware groups.
 *
 * @var array
 */
protected $middlewareGroups = [
    'web' => [
        //...
        \Illuminate\Session\Middleware\StartSession::class,
        \App\Http\Middleware\CustomSessionLifetime::class,
        //...
    ],

then I am unable to configure the session.lifetime config value because the session is already started and the old value has been used there.

I was kind of surprised that Laravel didn't have a native way to achieve something like this as I think it seems pretty standard to want to customize the session lifetime based on user somehow.

How could I make this work?

1

There are 1 answers

0
bloodleh On

I think I found a pretty decent workaround for this, by using two custom middlewares and a cookie.

BeforeStartSession.php middleware:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class BeforeStartSession
{
    public function handle(Request $request, Closure $next)
    {
        $cookieName = 'session_lifetime_cookie';
        $value = $request->cookie($cookieName, 0);
        $lifetime = (int) $value;

        if ($lifetime !== 0)
        {
            config()->set('session.lifetime', $lifetime);
        }

        return $next($request);
    }
}

AfterStartSession.php middleware:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cookie;

class AfterStartSession
{
    public function handle(Request $request, Closure $next)
    {
        $user = auth()->user();

        $cookieName = 'session_lifetime_cookie';
        $value = $request->cookie($cookieName, 0);
        $oldLifetime = (int) $value;

        if ($user !== null)
        {
            $newLifetime = $user->organization->session_lifetime;

            // Only add the cookie to the response if it doesn't exist
            // or the lifetime value has changed
            if ($oldLifetime === 0 || $oldLifetime !== $newLifetime)
            {
                Cookie::queue(Cookie::forever($cookieName, $newLifetime));
            }
        }
        else
        {
            // Only forget the cookie if it exists
            if ($oldLifetime !== 0)
            {
                Cookie::queue(Cookie::forget($cookieName));
            }
        }

        return $next($request);
    }
}

These are added to the $middlewareGroups array in the Kernel.php file so they are before and after the StartSession default middleware:

/**
 * The application's route middleware groups.
 *
 * @var array
 */
protected $middlewareGroups = [
    'web' => [
        //...
        \App\Http\Middleware\BeforeStartSession::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \App\Http\Middleware\AfterStartSession::class,
        //...
    ],

I also added the cookie manually in my login controller after succesfully logging the user in so the BeforeStartSession middleware picks up the correct value immediately after redirecting the user:

// Handle login
Auth::login($user);

$cookieName = 'session_lifetime_cookie';
Cookie::queue(Cookie::forever($cookieName, $user->organization->session_lifetime));

The only downside in this is that as cookies are stored client side, users are able to remove them (even if they are re-added after every request), decreasing the security of it. I countered this by having the default lifetime pretty low value.

This of course also adds some extra data to all requests as the Laravel cookie values are encrypted.