snicco / session
A standalone session implementation for environments where $_SESSION cant be used.
Requires
- php: ^7.4|^8.0
- ext-filter: *
- paragonie/constant_time_encoding: ^2.4
- snicco/str-arr: ^2.0
- snicco/testable-clock: ^2.0
Requires (Dev)
- cache/array-adapter: ^1.0.0
- phpunit/phpunit: ^9.5.13
- snicco/session-testing: ^2.0
Conflicts
- snicco/better-wp-api: <2.0.0-beta.9
- snicco/better-wp-cache: <2.0.0-beta.9
- snicco/better-wp-cache-bundle: <2.0.0-beta.9
- snicco/better-wp-cli: <2.0.0-beta.9
- snicco/better-wp-cli-testing: <2.0.0-beta.9
- snicco/better-wp-hooks: <2.0.0-beta.9
- snicco/better-wp-hooks-bundle: <2.0.0-beta.9
- snicco/better-wp-mail: <2.0.0-beta.9
- snicco/better-wp-mail-bundle: <2.0.0-beta.9
- snicco/better-wp-mail-testing: <2.0.0-beta.9
- snicco/better-wpdb: <2.0.0-beta.9
- snicco/better-wpdb-bundle: <2.0.0-beta.9
- snicco/blade-bridge: <2.0.0-beta.9
- snicco/blade-bundle: <2.0.0-beta.9
- snicco/content-negotiation-middleware: <2.0.0-beta.9
- snicco/debug-bundle: <2.0.0-beta.9
- snicco/default-headers-middleware: <2.0.0-beta.9
- snicco/eloquent: <2.0.0-beta.9
- snicco/encryption-bundle: <2.0.0-beta.9
- snicco/event-dispatcher: <2.0.0-beta.9
- snicco/event-dispatcher-testing: <2.0.0-beta.9
- snicco/guests-only-middleware: <1.0.0
- snicco/http-routing: <2.0.0-beta.9
- snicco/http-routing-bundle: <2.0.0-beta.9
- snicco/http-routing-testing: <2.0.0-beta.9
- snicco/https-only-middleware: <2.0.0-beta.9
- snicco/illuminate-container-bridge: <2.0.0-beta.9
- snicco/kernel: <2.0.0-beta.9
- snicco/kernel-testing: <2.0.0-beta.9
- snicco/method-override-middleware: <2.0.0-beta.9
- snicco/minimal-logger: <2.0.0-beta.9
- snicco/must-match-route-middleware: <2.0.0-beta.9
- snicco/no-robots-middleware: <2.0.0-beta.9
- snicco/open-redirect-protection-middleware: <2.0.0-beta.9
- snicco/payload-middleware: <2.0.0-beta.9
- snicco/pimple-bridge: <2.0.0-beta.9
- snicco/psr7-error-handler: <2.0.0-beta.9
- snicco/redirect-middleware: <2.0.0-beta.9
- snicco/session-bundle: <2.0.0-beta.9
- snicco/session-psr16-bridge: <2.0.0-beta.9
- snicco/session-testing: <2.0.0-beta.9
- snicco/session-wp-bridge: <2.0.0-beta.9
- snicco/share-cookies-middleware: <2.0.0-beta.9
- snicco/signed-url: <2.0.0-beta.9
- snicco/signed-url-psr15-bridge: <2.0.0-beta.9
- snicco/signed-url-psr16-bridge: <2.0.0-beta.9
- snicco/signed-url-testing: <2.0.0-beta.9
- snicco/signed-url-wp-bridge: <2.0.0-beta.9
- snicco/templating: <2.0.0-beta.9
- snicco/templating-bundle: <2.0.0-beta.9
- snicco/testing-bundle: <2.0.0-beta.9
- snicco/trailing-slash-middleware: <2.0.0-beta.9
- snicco/wp-auth-only-middleware: <2.0.0-beta.9
- snicco/wp-capability-middleware: <2.0.0-beta.9
- snicco/wp-capapility-middleware: <1.0.0
- snicco/wp-guests-only-middleware: <2.0.0-beta.9
- snicco/wp-nonce-middleware: <2.0.0-beta.9
- dev-master
- v2.0.0-beta.9
- v2.0.0-beta.8
- v2.0.0-beta.7
- v2.0.0-beta.6
- v2.0.0-beta.5
- v2.0.0-beta.4
- v2.0.0-beta.3
- v2.0.0-beta.2
- v2.0.0-beta.1
- v1.10.1
- v1.10.0
- v1.9.1
- v1.9.0
- v1.8.1
- v1.8.0
- v1.7.0
- v1.6.2
- v1.6.1
- v1.6.0
- v1.5.0
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.0
- v1.2.1
- v1.2.0
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.2
- v1.0.1
- v1.0.0
- dev-beta
This package is auto-updated.
Last update: 2024-11-07 14:50:07 UTC
README
Table of contents
- Motivation
- Installation
- Usage
- Configuration
- Creating a serializer
- Drivers
- Creating the session manager
- Starting a session
- The immutable session
- The mutable session
- Accessing nested data
- Flash messages / Old input
- Encrypting session data
- Saving a session
- Setting the session cookie
- Managing sessions based on user id
- Garbage collection
- Contributing
- Issues and PR's
- 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 orserializing
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:
- The
JsonSerializer
, which assumes that all your session content isJsonSerializable
or equivalent. - The
PHPSerializer
, which will useserialize
andunserialize
.
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 anotherSessionDriver
as an argument and encrypts/decrypts its data. -
Psr16Driver
, allows you to use any PSR-16 cache. You can use this driver by using thesnicco/session-psr16-bridge
. -
WPDBDriver
, you can usesnicco/session-wp-bridge
to store sessions using the WordPress database. -
WP_Object_Cache
you can usesnicco/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 usesnicco/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:
- Rejecting the session id and generating a new, empty session, if the provided id can't be found in the driver (or is absent).
- Rotating the session id based on your configuration.
- 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', 'calvin@snicco.io'); // 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.