Skip to content

Latest commit

 

History

History
557 lines (378 loc) · 18.2 KB

README.md

File metadata and controls

557 lines (378 loc) · 18.2 KB

Snicco Session: A custom session implementation for environments where $_SESSION can't be used

codecov Psalm Type-Coverage Psalm level PhpMetrics - Static Analysis PHP-Versions

Table of contents

  1. Motivation
  2. Installation
  3. Usage
    1. Configuration
    2. Creating a serializer
    3. Drivers
    4. Creating the session manager
    5. Starting a session
    6. The immutable session
    7. The mutable session
    8. Accessing nested data
    9. Flash messages / Old input
    10. Encrypting session data
    11. Saving a session
    12. Setting the session cookie
    13. Managing sessions based on user id
    14. Garbage collection
  4. Contributing
  5. Issues and PR's
  6. Security

Motivation

While PHP's native $_SESSION is fine for most use cases there are certain environments where it's not ideal. Two of them being distributed WordPress code or PSR7/PSR15 applications.

The Session component of the Snicco project is a completely standalone library with zero dependencies on any framework.

Features:

  • Automatically handles invalidation, rotation and idle-timeouts.

  • Non-blocking.

  • Tracks if sessions are dirty and only updates if needed (without affected timeouts).

  • Only accepts server-side generated session IDs.

  • Supports many storage backends, all in their separate composer packages.

  • Uses paragonie's split token approach to protect against timing based side-channel attacks.

  • Secure by design, it's not possible to hijack session ids by compromising the storage backend (assuming read-only access)

  • PSR-7/15 compatible. No hidden dependencies on PHP super globals.

  • Differentiation between mutable and immutable session objects.

  • Choose between json_encoding your session data or serializing it. Or provide your own normalizer.

  • Supports encrypting and decrypting session data (through an interface, don't panic).

  • Advanced session management based on user ids.

  • Support for flash messages and old input.

  • 100% test coverage and 100% psalm type-coverage.

Installation

composer require snicco/session

Usage

Creating a session configuration

use Snicco\Component\Session\ValueObject\SessionConfig;

$configuration = new SessionConfig([
    // The path were the session cookie will be available
    'path' => '/',
    // The session cookie name
    'cookie_name' => 'my_app_sessions',
    // This should practically never be set to false
    'http_only' => true,
    // This should practically never be set to false
    'secure' => true,
    // one of "Lax"|"Strict"|"None"
    'same_site' => 'Lax',
    // A session with inactivity greater than the idle_timeout will be regenerated and flushed
    'idle_timeout_in_sec' => 60 * 15,
    // Rotate session ids periodically
    'rotation_interval_in_sec' => 60 * 10,
    // Setting this value to NULL will make the session a "browser session".
    // Setting this to any positive integer will mean that the session will be regenerated and flushed
    // independently of activity.
    'absolute_lifetime_in_sec' => null,
    // The percentage that any given call to SessionManager::gc() will trigger garbage collection
    // of inactive sessions.
    'garbage_collection_percentage' => 2,
]);

Creating a serializer

This package comes with two inbuilt serializers:

  1. The JsonSerializer, which assumes that all your session content is JsonSerializableor equivalent.
  2. The PHPSerializer, which will use serialize and unserialize.

If these don't work you, simply implement the Serializer interface.


Creating a session driver

The SessionDriver is an interface that abstracts away the concrete storage backend for the session data.

Currently, the following drivers are available:

  • InMemoryDriver, for usage during testing.

  • EncryptedDriver, takes another SessionDriver as an argument and encrypts/decrypts its data.

  • Psr16Driver, allows you to use any PSR-16 cache. You can use this driver by using the snicco/session-psr16-bridge.

  • WPDBDriver, you can use snicco/session-wp-bridge to store sessions using the WordPress database.

  • WP_Object_Cache you can use snicco/session-wp-bridge to store sessions using the WordPress object cache.

  • Custom, if none of these drivers work for you (and there is no PSR-16 adapter) you can use snicco/session-testing to test a custom implementation of yours against the interface.


Creating a session manager

The SessionManager is responsible for creating and persisting Session objects.

use Snicco\Component\Session\SessionManager\SessionManger;

$configuration = /* */
$serializer = /* */
$driver = /* */

$session_manger = new SessionManger($configuration, $driver, $serializer);

Starting a session

The SessionManager uses an instance of CookiePool to start a session.

You can instantiate this object either from the $_COOKIE superglobal or any plain array.

Calling SessionManger::start() will handle:

  1. Rejecting the session id and generating a new, empty session, if the provided id can't be found in the driver (or is absent).
  2. Rotating the session id based on your configuration.
  3. Rotating and clearing the session if the session is idle based on your configuration.
use Snicco\Component\Session\SessionManager\SessionManger;
use Snicco\Component\Session\ValueObject\CookiePool;

$configuration = /* */
$serializer = /* */
$driver = /* */

$session_manger = new SessionManger($configuration, $driver, $serializer);

// using $_COOKIE
$cookie_pool = CookiePool::fromSuperGlobals();

// or any array.
$cookie_pool = new CookiePool($psr7_request->getCookieParams());


$session = $session_manger->start($cookie_pool);

Calling SessionManager::start() will return an instance of Session. Session is an interface that extends both the MutableSession interface and the ImmutableSession interface.

This allows you to clearly separate the different concerns of reading and writing to the session.

In your code you should either depend on MutableSession or ImmutableSession.

The Session interface is only needed to persist the session with the session manager.


The immutable session

The ImmutableSession only has methods that return data. There is no way to modify the session.

use Snicco\Component\Session\ImmutableSession;
use Snicco\Component\Session\Session;
use Snicco\Component\Session\ValueObject\ReadOnlySession;

/**
* @var Session $session 
*/
$session = $session_manger->start($cookie_pool);

// You can either rely on type-hints or transform $session to an immutable object like so:
$read_only_session = ReadOnlySession::fromSession($session);

function readFromSession(ImmutableSession $session) {
    
    $session->id(); // instance of SessionId
    
    $session->isNew(); // true/false
        
    $session->userId(); // int|string|null
        
    $session->createdAt(); // timestamp. Can never be changed.
        
    $session->lastRotation(); // timestamp
    
    $session->lastActivity(); // last activity is updated each time a session is saved.
    
    $session->has('foo'); // true/false
    
    $session->boolean('wants_beta_features'); // true/false
    
    $session->only(['foo', 'bar']); // only get keys "foo" and "bar"
    
    $session->get('foo', 'default'); // get key "foo" with optional default value
    
    $session->all(); // Returns array of all user provided data.
    
    $session->oldInput('username', ''); // Old input is flushed after saving a session twice.
    
    $session->hasOldInput('username'); // true/false
    
    $session->missing(['foo', 'bar']); // Returns true if all the given keys are not in the session.
    
    $session->missing(['foo', 'bar']); // Returns true if all the given keys are in the session.
    
}

The mutable session

The Mutable only has methods that modify data. There is no way to read the session data.

use Snicco\Component\Session\MutableSession;
use Snicco\Component\Session\Session;
use Snicco\Component\Session\ValueObject\ReadOnlySession;

/**
* @var Session $session 
*/
$session = $session_manger->start($cookie_pool);

function modifySession(MutableSession $session) {
    
    // Store the current user after authentication.
    $session->setUserId('user-1');
    // can be int|string
    $session->setUserId(1);
    
    // Rotates the session id and flushes all data.
    $session->invalidate();
        
    // Rotates the session id WITHOUT flushing data.
    $session->rotate(); 
        
    $session->put('foo', 'bar');
    $session->put(['foo' => 'bar', 'baz' => 'biz']);
        
    $session->putIfMissing('foo', 'bar');
    
    $session->increment('views');
    $session->increment('views', 2); // Increment by 2
    
    $session->decrement('views');
    $session->decrement('views', 2); // Decrement by 2
    
    $session->push('viewed_pages', 'foo-page'); // Push a value onto an array.
    
    $session->remove('foo');
    
    $session->flash('account_created', 'Your account was created'); // account_created is only available during the current request and the next request.
    
    $session->flashNow('account_created', 'Your account was created' ); // account_created is only available during the current request.
    
    $session->flashInput('login_form.email', '[email protected]'); // This value is available during the current request and the next request.

    $session->reflash(); // Reflash all flash data for one more request.
    
    $session->keep(['account_created']); // Keep account created for one more request.
    
    $session->flush(); // Empty the session data.
    
}

Accessing nested data

Nested data can be accessed using "dots".

$session->put([
    'foo' => [
        'bar' => 'baz'
    ]   
]);

var_dump($session->get('foo.bar')); // baz

Flash messages / Old input

Flashing data to the session means storing it only until the session is saved twice.

The most common use case for this is to display toast notifications after a POST request.

// POST request: 

// create user account and redirect to success page.

$session->flash('account_created', 'Great! Your account was created.');

// session is saved.

// GET request:

echo $session->get('account_created');

// session is saved again, account_created is now gone.

Old input works very similar. The most common use case is to display submitted form data on failure to validate the form.

// POST request: 

$username = $_POST['username'];

// validate the request...

// Validation failed.
$session->flashInput('username', $username);

// session is saved.

// GET request:

if($session->hasOldInput('username')) {
    $username = $session->oldInput('username');
    // Use username to populate the form values again.
}

// session is saved again, username is now gone.

Encrypting session data

If you are storing sensitive data in your session you can use the EncryptedDriver.

This driver will wrap another (inner) session driver and encrypt/decrypt your data before passing it to your application code.

To function, the EncryptedDriver needs an instance of SessionEncryptor, which is a dead-simple interface with no implementation.

Here is how you would use defuse/php-encryption to encrypt your sessions.

use Snicco\Component\Session\Driver\EncryptedDriver;
use Snicco\Component\Session\SessionEncryptor;

final class DefuseSessionEncryptor implements SessionEncryptor
{
    private string $key;

    public function __construct(string $key)
    {
        $this->$key = $key;
    }

    public function encrypt(string $data): string
    {
        return Defuse\Crypto\Crypto::encrypt($data, $this->key);
    }

    public function decrypt(string $data): string
    {
       return Defuse\Crypto\Crypto::decrypt($data, $this->key);
    }
}

$driver = new EncryptedDriver(
    $inner_driver,
    new DefuseSessionEncryptor($your_key)
)

Saving a session

Session is a value object. Changes in the session are only persisted when the session manager saves it.

Once a Session is saved it is locked. Calling any state changing methods on a locked session will throw a SessionIsLocked exception.

Calling save on an unmodified session will only update the last activity of the session using SessionDriver::touch() .

This eliminates a lot a race-conditions that might happen with overlapping GET/POST requests that read and write a session.

use Snicco\Component\Session\SessionManager\SessionManger;
use Snicco\Component\Session\ValueObject\CookiePool;

$configuration = /* */
$serializer = /* */
$driver = /* */
$cookie_pool = /* */;

$session_manger = new SessionManger($configuration, $driver, $serializer);

$session = $session_manger->start($cookie_pool);

$session->put('foo', 'bar');

$session_manger->save($session);

// This will throw an exception.
$session->put('foo', 'baz');

Setting the session cookie

Setting cookies is out of scope for this library (because we don't know how you handle HTTP concerns in your application).

Instead, the session manager provides a method to retrieve a SessionCookie value object from a session.

An example on how to use the SessionCookie class to set the session cookie using setcookie. You can do something similar if you are using PSR-7 requests.

use Snicco\Component\Session\SessionManager\SessionManger;
use Snicco\Component\Session\ValueObject\CookiePool;

$configuration = /* */
$serializer = /* */
$driver = /* */
$cookie_pool = /* */;

$session_manger = new SessionManger($configuration, $driver, $serializer);

$session = $session_manger->start($cookie_pool);

$session->put('foo', 'bar');

$session_manger->save($session);

$cookie = $session_manger->toCookie($session);

$same_site = $cookie->sameSite();
$same_site = ('None; Secure' === $same_site) ? 'None' : $same_site;

setcookie($cookie->name(), $cookie->value(), [
    'expires' => $cookie->expiryTimestamp(),
    'samesite' => $same_site,
    'secure' => $cookie->secureOnly(),
    'path' => $cookie->path(),
    'httponly' => $cookie->httpOnly(),
]);

Managing session based on user id

It's not a requirement to store user ids in your session.

However, if you choose so, this package provides some nice tools to manage sessions based on user ids.

The UserSessionsDriver extends the SessionDriver interface.

Not all drivers support this interface tho.

use Snicco\Component\Session\Driver\InMemoryDriver;

// The in memory driver implements UserSessionDriver
$in_memory_driver = new InMemoryDriver();

// Destroy all sessions, for all users.
$in_memory_driver->destroyAllForAllUsers();

// Destroys all sessions where the user id has been set to (int) 12.
// Useful for "log me out everywhere" functionality.
$in_memory_driver->destroyAllForUserId(12);

$session_selector = $session->id()->selector();
// Destroys all sessions for user 12 expect the passed one.
// Useful for "log me out everywhere else" functionality.
$in_memory_driver->destroyAllForUserIdExcept($session_selector, 12);

// Returns an array of SerializedSessions for user 12.
$in_memory_driver->getAllForUserId(12);

Garbage collection

You should call SessionManager::gc() on every request where you use sessions.

// That's it, this will remove all idle sessions with the percentage that you configured.
$session_manager->gc();

Contributing

This repository is a read-only split of the development repo of the Snicco project.

This is how you can contribute.

Reporting issues and sending pull requests

Please report issues in the Snicco monorepo.

Security

If you discover a security vulnerability, please follow our disclosure procedure.