psr7-sessions / storageless

:mailbox_with_mail: storage-less PSR-7 session support
MIT License
648 stars 38 forks source link

PSR-7 Storage-less HTTP Sessions

Mutation testing badge Type Coverage Packagist Packagist

PSR7Session is a PSR-7 and PSR-15 compatible middleware that enables session without I/O usage in PSR-7 based applications.

Proudly brought to you by ocramius, malukenho and lcobucci.

Installation

composer require psr7-sessions/storageless

Usage

You can use the PSR7Sessions\Storageless\Http\SessionMiddleware in any PSR-15 compatible middleware.

In a mezzio/mezzio application, this would look like following:

use Lcobucci\JWT\Configuration as JwtConfig;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key\InMemory;
use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Http\Configuration as StoragelessConfig;

$app = new \Mezzio\Application(/* ... */);

$app->pipe(new SessionMiddleware(
    new StoragelessConfig(
        JwtConfig::forSymmetricSigner(
            new Signer\Hmac\Sha256(),
            InMemory::base64Encoded('OpcMuKmoxkhzW0Y1iESpjWwL/D3UBdDauJOe742BJ5Q='), // replace this with a key of your own (see below)
        )
    )
));

After this, you can access the session data inside any middleware that has access to the Psr\Http\Message\ServerRequestInterface attributes:

$app->get('/get', function (ServerRequestInterface $request, ResponseInterface $response) : ResponseInterface {
    /** @var \PSR7Sessions\Storageless\Session\SessionInterface $session */
    $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
    $session->set('counter', $session->get('counter', 0) + 1);

    $response
        ->getBody()
        ->write('Counter Value: ' . $session->get('counter'));

    return $response;
});

You can do this also in asynchronous contexts and long-running processes, since no super-globals nor I/O are involved.

It is recommended that you use a key with lots of entropy, preferably generated using a cryptographically secure pseudo-random number generator (CSPRNG). You can use the CryptoKey tool to do this for you.

Note that you can also use asymmetric keys; please refer to lcobucci/jwt documentation:

  1. The Configuration object: https://lcobucci-jwt.readthedocs.io/en/stable/configuration/
  2. Supported algorithms: https://lcobucci-jwt.readthedocs.io/en/stable/supported-algorithms/

Session Hijacking mitigation

To mitigate the risks associated to cookie stealing and thus session hijacking, you can bind the user session to its IP ($_SERVER['REMOTE_ADDR']) and User-Agent ($_SERVER['HTTP_USER_AGENT']) by enabling client fingerprinting:

use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Http\Configuration as StoragelessConfig;
use PSR7Sessions\Storageless\Http\ClientFingerprint\Configuration as FingerprintConfig;

$app = new \Mezzio\Application(/* ... */);

$app->pipe(new SessionMiddleware(
    (new StoragelessConfig(/* ... */))
        ->withClientFingerprintConfiguration(
            FingerprintConfig::forIpAndUserAgent()
        )
));

If your PHP service is behind a reverse proxy of yours, you may need to retrieve the client IP from a different source of truth. In such cases you can extract the information you need by writing a custom \PSR7Sessions\Storageless\Http\ClientFingerprint\Source implementation:

use Psr\Http\Message\ServerRequestInterface;
use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Http\Configuration as StoragelessConfig;
use PSR7Sessions\Storageless\Http\ClientFingerprint\Configuration as FingerprintConfig;
use PSR7Sessions\Storageless\Http\ClientFingerprint\Source;

$app = new \Mezzio\Application(/* ... */);

$app->pipe(new SessionMiddleware(
    (new StoragelessConfig(/* ... */))
        ->withClientFingerprintConfiguration(
            FingerprintConfig::forSources(new class implements Source{
                 public function extractFrom(ServerRequestInterface $request): string
                 {
                     return $request->getHeaderLine('X-Real-IP');
                 }
            })
        )
));

Examples

Simply browse to the examples directory in your console, then run

php -S localhost:9999 index.php

Then try accessing http://localhost:9999: you should see a counter that increases at every page refresh

WHY?

In most PHP+HTTP related projects, ext/session serves its purpose and allows us to store server-side information by associating a certain identifier to a visiting user-agent.

What is the problem with ext/session?

This is all fair and nice, except for:

What does this project do?

This project tries to implement storage-less sessions and to mitigate the issues listed above.

Assumptions

How does it work?

Session data is directly stored inside a session cookie as a JWT token.

This approach is not new, and is commonly used with Bearer tokens in HTTP/REST/OAuth APIs.

In order to guarantee that the session data is not modified, that the client can trust the information and that the expiration date is mutually agreed between server and client, a JWT token is used to transmit the information.

The JWT token is always signed to ensure that the user-agent is never able to manipulate the session. Both symmetric and asymmetric keys are supported for signing/verifying tokens.

Advantages

Configuration options

Please refer to the configuration documentation.

Known limitations

Please refer to the limitations documentation.

Contributing

Please refer to the contributing notes.

License

This project is made public under the MIT LICENSE.