chmod 755

PHP Native Session with Laravel

October 18, 2020

Completely replacing legacy code is hard. Rebuilding years of work doesn’t happen overnight. The strangler pattern offers a strategy to slowly and gradually replace a legacy codebase with a fresh one piece by piece. One of the key aspects of getting a successful project get started in replacing a legacy codebase was keeping the authentication system working. Let me set the scene.

Context

A 15-year old codebase written in PHP using $_SESSION across several thousands of files lives behind a web server running under api.mydomain.com. It acts as a backend api for a frontend application that establish these API calls using PHP browser’s cookie (PHPSESSID). We can start a brand new project and call it api.v2.mydomain.com or we can use path-based routing and define an ALB routing on AWS where api.mydomain.com/v2 automatically routes to this second project. The new project can take advantage of modern php development (a public framework, composer, tdd, etc), but developing a brand new authentication system is a huge undertake at this point. It requires changes to the authentication process, the frontend and the backend project. A less disruptive process would be to simply focus on making the v2 project viable while keeping as much backward compatibility as possible.

Laravel Authentication with $_SESSION

Laravel PHP Session is a package that helps achieve exactly that. We install it via composer require customergauge/session and configure auth.php with a new authentication mechanism.

    'guards' => [
        'php' => [
            'driver' => \CustomerGauge\Session\NativeSessionGuard::class,
            'provider' => \CustomerGauge\Session\NativeSessionUserProvider::class,
            'domain' => '.mydomain.com',
            'storage' => 'tcp://my-redis-address:6379',
        ],
    ],

The domain attribute is directly mapped to PHP’s COOKIE_DOMAIN ini setting and the storage is mapped to SESSION_PATH.

The next step is to implement the UserFactory interface and bind it into Laravel’s Container.

final class NativeSessionUserFactory implements UserFactory
{
    public function make(array $session): ?Authenticatable
    {
        // $session here is the same as $_SESSION
        if (empty($session['id'])) {
            return null;
        }

        return new MyUser(
            (int) $session['id'],
            $session['my_session_attribute']
        );
    }
}

Laravel’s auth middleware allows us to specify a guard when declaring a route as authenticatable. See more on https://laravel.com/docs/8.x/authentication#protecting-routes. As long as our route is protected by auth:php, the authentication process will be triggered.

Test Code

Laravel already provides the actingAs functionality in it’s TestCase classes. That functionality will allow us to bind a specific user object into the container so that the application treat the execution test as authenticated. To avoid any problems with the PHP Native session, we can also bind a mock instance of SessionRetriever into the container

    protected function setUp(): void
    {
        parent::setUp();

        $fakeSession = ['id' => 'my-fake-$_SESSION-content'];

        $this->app->instance(SessionRetriever::class, SessionRetriever::fake($fakeSession));
    }

This bind will prevent any execution path that might lead to SessionRetriever calling sesion_start(), which would fail under phpunit as there’s no headers to be sent.

Conclusion

I’ve been using this strategy for well over 3 years now and it empowered my team to strangle more and more our legacy codebase into a modern application in small, incremental steps. The more code we move to services designed with this mindset, the less $_SESSION spread across a codebase we have and the closer we get to having a consistent authentication mechanism that might easily be replaced by another Laravel Guard. As a matter of fact, we already have a Laravel Guard for AWS Cognito that I wrote about here.

As always, hit me up on Twitter with any questions.

Cheers.


Marco Aurélio Deleu

Marco Aurélio Deleu
Writing bad code for 10 years. Passionate about Laravel and AWS.

twitter