diff --git a/docs/book/v2/installation.md b/docs/book/v2/installation.md new file mode 100644 index 0000000..be1198c --- /dev/null +++ b/docs/book/v2/installation.md @@ -0,0 +1,5 @@ +# This Is Only a Placeholder + +The content of this page can be found under: + +https://github.com/laminas/documentation-theme/blob/master/theme/pages/installation.html diff --git a/docs/book/v2/intro.md b/docs/book/v2/intro.md new file mode 100644 index 0000000..57fe4f5 --- /dev/null +++ b/docs/book/v2/intro.md @@ -0,0 +1,73 @@ +# mezzio-session + +Web applications often need to persist user state between requests, and the +generally accepted way to do so is via _sessions_. While PHP provides its own +session extension, it: + +- uses global functions that affect global state. +- relies on a superglobal for access to both read and write the session data. +- incurs either filesystem or network I/O on every request, depending on the + session storage handler. +- can clobber the `Set-Cookie` header when other processes also set it. + +Some projects, such as [psr-7-sessions/storageless](https://github.com/psr7-sessions/storageless), +take a different approach, using [JSON Web Tokens](https://tools.ietf.org/html/rfc7519) (JWT). + +The goals of mezzio-session are: + +- to abstract the way users interact with session storage. +- to abstract how sessions are persisted, to allow both standard ext-session, + but also other paradigms such as JWT. +- to provide session capabilities that "play nice" with + [PSR-7](http://www.php-fig.org/psr/psr-7/) and middleware. + +## Installation + +Use [Composer](https://getcomposer.org) to install this package: + +```bash +$ composer require mezzio/mezzio-session +``` + +However, the package is not immediately useful unless you have a persistence +adapter. If you are okay with using ext-session, you can install the following +package as well: + +```bash +$ composer require mezzio/mezzio-session-ext +``` + +## Features + +mezzio-session provides the following: + +- Interfaces for: + - session containers + - session persistence +- An implementation of the session container. +- A "lazy-loading" implementation of the session container, to allow delaying + any de/serialization and/or I/O processes until session data is requested; + this implementation decorates a normal session container. +- PSR-7 middleware that: + - composes a session persistence implementation. + - initializes the lazy-loading session container, using the session + persistence implementation. + - delegates to the next middleware, passing the session container into the + request. + - finalizes the session before returning the response. + +Persistence implementations locate session information from the requests (e.g., +via a cookie) in order to initialize the session. On completion of the request, +they examine the session container for changes and/or to see if it is empty, and +provide data to the response so as to notify the client of the session (e.g., +via a `Set-Cookie` header). + +Note that the goals of this package are solely focused on _session persistence_ +and _access to session data by middleware_. If you also need other features +often related to session data, you may want to consider the following packages: + +- [mezzio-flash](https://github.com/mezzio/mezzio-flash): + provides flash message capabilities. +- [mezzio-csrf](https://github.com/mezzio/mezzio-csrf): + provides CSRF token generation, storage, and verification, using either a + session container, or flash messages. diff --git a/docs/book/v2/middleware.md b/docs/book/v2/middleware.md new file mode 100644 index 0000000..be60cd9 --- /dev/null +++ b/docs/book/v2/middleware.md @@ -0,0 +1,142 @@ +# Session Middleware + +mezzio-session provides middleware consuming +[PSR-7](http://www.php-fig.org/psr/psr-7/) HTTP message instances, via +implementation of [PSR-15](https://www.php-fig.org/psr/psr-15/) +interfaces. + +This middleware composes a [persistence](persistence.md) instance, and uses that +in order to generate a session container, which it pushes into the request it +delegates to the next middleware. Once a response is returned, it uses the +persistence instance to persist the session data and provide information back to +the client. + +The above two paragraphs are longer than the body of the middleware +implementation: + +```php +namespace Mezzio\Session; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class SessionMiddleware implements MiddlewareInterface +{ + public const SESSION_ATTRIBUTE = 'session'; + + private $persistence; + + public function __construct(SessionPersistenceInterface $persistence) + { + $this->persistence = $persistence; + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + $session = new LazySession($this->persistence, $request); + $response = $handler->handle( + $request + ->withAttribute(self::SESSION_ATTRIBUTE, $session) + ->withAttribute(SessionInterface::class, $session) + ); + return $this->persistence->persistSession($session, $response); + } +} +``` + +## Configuration + +This package provides a factory for `Mezzio\Session\SessionMiddleware` +via `Mezzio\Session\SessionMiddlewareFactory`; this factory is +auto-wired if you are using Mezzio and the laminas-component-installer Composer +plugin. If not, you will need to wire these into your application. + +The factory depends on one service: `Mezzio\Session\SessionPersistenceInterface`. +You will need to either wire in your persistence implementation of choice, or +have the package providing it do so for you. + +## Adding the middleware to your application + +You may pipe this middleware anywhere in your application. If you want to have +it available anywhere, pipe it early in your application, prior to any routing. +As an example, within Mezzio, you could pipe it in the `config/pipeline.php` +file: + +```php +$app->pipe(\Mezzio\Session\SessionMiddleware::class); +$app->pipe(\Mezzio\Router\Middleware\RouteMiddleware::class); +``` + +This will generally be an inexpensive operation; since the middleware uses a +`LazySession` instance, unless your persistence implementation does any work in +its constructor, the cost is just that of instantiating a few objects. + +However, it's often useful to specifically include such middleware directly in +the routed middleware pipelines, to ensure other developers are aware of its +presence in that route's workflow. + +Within Mezzio, you can do this when routing, in your `config/routes.php` +file, or within a [delegator factory](https://docs.mezzio.dev/mezzio/cookbook/autowiring-routes-and-pipelines/#delegator-factories): + +```php +$app->post('/login', [ + \Mezzio\Session\SessionMiddleware::class, + \User\Middleware\LoginHandler::class +]); +``` + +## Retrieving the session in your own middleware + +Whilst it is trivial to retrieve the initialised session from the request with `$session = $request->getAttribute(SessionInterface::class);`, static analysers cannot automatically infer the value assigned to `$session`. + +To provide a convenient and type safe way to retrieve the session from the current request without manually asserting its type, `SessionRetrieval::fromRequest($request)` can be called so that you can use the request without further assertions. + +Furthermore, a static method exists to optionally retrieve a session when you cannot be sure the middleware has previously been piped: `SessionRetrieval::fromRequestOrNull($request)` + +```php +namespace My\NameSpace; + +use Mezzio\Session\Exception\SessionNotInitializedException; +use Mezzio\Session\RetrieveSession; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class MyRequestHandler implements RequestHandlerInterface { + + // ... + + public function handle(ServerRequestInterface $request) : ResponseInterface + { + try { + $session = RetrieveSession::fromRequest($request); + } catch (SessionNotInitializedException $error) { + // Handle the uninitialized session: + return $this->redirectToLogin(); + } + + $value = $session->get('SomeKey'); + $this->templateRenderer->render('some:template', ['value' => $value]); + } +} + +class AnotherRequestHandler implements RequestHandlerInterface { + + // ... + + public function handle(ServerRequestInterface $request) : ResponseInterface + { + $session = RetrieveSession::fromRequestOrNull($request); + if (! $session) { + // Handle the uninitialized session: + return $this->redirectToLogin(); + } + + $value = $session->get('SomeKey'); + $this->templateRenderer->render('some:template', ['value' => $value]); + } +} + +``` diff --git a/docs/book/v2/persistence.md b/docs/book/v2/persistence.md new file mode 100644 index 0000000..9714da2 --- /dev/null +++ b/docs/book/v2/persistence.md @@ -0,0 +1,108 @@ +# Session Persistence + +Session persistence within mezzio-session refers to one or both of the +following: + +- Identifying session information provided by the client making the request. +- Storing session data for access on subsequent requests. +- Providing session information to the client making the request. + +In some scenarios, such as usage of JSON Web Tokens (JWT), the serialized +session data is provided _by_ the client, and provided _to_ the client directly, +without any server-side storage whatsoever. + +To describe these operations, we provide `Mezzio\Session\SessionPersistenceInterface`: + +```php +namespace Mezzio\Session; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +interface SessionPersistenceInterface +{ + /** + * Generate a session data instance based on the request. + */ + public function initializeSessionFromRequest(ServerRequestInterface $request) : SessionInterface; + + /** + * Persist the session data instance. + * + * Persists the session data, returning a response instance with any + * artifacts required to return to the client. + */ + public function persistSession(SessionInterface $session, ResponseInterface $response) : ResponseInterface; +} +``` + +Session initialization pulls data from the request (a cookie, a header value, +etc.) in order to produce a session container. Session persistence pulls data +from the session container, does something with it, and then optionally provides +a response containing session artifacts (a cookie, a header value, etc.). + +For sessions to work, _you must provide a persistence implementation_. We +provide one such implementation using PHP's session extension via the package +[mezzio-session-ext](https://github.com/mezzio/mezzio-session-ext). + +## Session identifiers + +Typically, the session identifier will be retrieved from the request (usually +via a cookie), and a new identifier created if none was discovered. + +During persistence, if an existing session's contents have changed, or +`regenerateId()` was called on the session, the persistence implementation +becomes responsible for: + +- Removing the original session. +- Generating a new identifier for the session. + +In all situations, it then needs to store the session data in such a way that a +later lookup by the current identifier will retrieve that data. + +Prior to version 1.1.0, persistence engines had two ways to determine what the +original session identifier was when it came time to regenerate or persist a +session: + +- Store the identifier as a property of the persistence implementation. +- Store the identifier in the session data under a "magic" key (e.g., + `__SESSION_ID__`). + +The first approach is problematic when using mezzio-session in an async +environment such as [Swoole](https://swoole.co.uk) or +[ReactPHP](https://reactphp.org), as the same persistence instance may be used +by several simultaneous requests. `Mezzio\Session\SessionInterface` defines a new +`getId` method, implementations can thus store the +identifier internally, and, when it comes time to store the session data, +persistence implementations can query that method in order to retrieve the +session identifier. + +## Persistent sessions + +- Since 1.2.0. + +If your persistence implementation supports persistent sessions — for +example, by setting an `Expires` or `Max-Age` cookie directive — then you +can opt to globally set a default session duration, or allow developers to hint +a desired session duration via the session container using +`SessionContainerPersistenceInterface::persistSessionFor()`. + +Implementations SHOULD honor the value of `SessionContainerPersistenceInterface::getSessionLifetime()` +when persisting the session data. This could mean either or both of the +following: + +- Ensuring that the session data will not be purged until after the specified + TTL value. +- Setting an `Expires` or `Max-Age` cookie directive. + +In each case, the persistence engine should query the `Session` instance for a +TTL value: + +```php +$ttl = $session instanceof SessionContainerPersistenceInterface + ? $session->getSessionLifetime() + : $defaultLifetime; // likely 0, to indicate automatic expiry +``` + +`getSessionLifetime()` returns an `integer` value indicating the number of +seconds the session should persist. diff --git a/docs/book/v2/session.md b/docs/book/v2/session.md new file mode 100644 index 0000000..0dbe0df --- /dev/null +++ b/docs/book/v2/session.md @@ -0,0 +1,252 @@ +# Session Containers + +Session containers are the primary interface with which most application +developers will work; they contain the data currently in the session, and allow +you to push data to the session. + +All session containers implement `Mezzio\Session\SessionInterface`: + +```php +namespace Mezzio\Session; + +interface SessionInterface +{ + /** + * Serialize the session data to an array for storage purposes. + */ + public function toArray() : array; + + /** + * Retrieve a value from the session. + * + * @param mixed $default Default value to return if $name does not exist. + * @return mixed + */ + public function get(string $name, $default = null); + + /** + * Whether or not the container has the given key. + */ + public function has(string $name) : bool; + + /** + * Set a value within the session. + * + * Values MUST be serializable in any format; we recommend ensuring the + * values are JSON serializable for greatest portability. + * + * @param mixed $value + */ + public function set(string $name, $value) : void; + + /** + * Remove a value from the session. + */ + public function unset(string $name) : void; + + /** + * Clear all values. + */ + public function clear() : void; + + /** + * Does the session contain changes? If not, the middleware handling + * session persistence may not need to do more work. + */ + public function hasChanged() : bool; + + /** + * Regenerate the session. + * + * This can be done to prevent session fixation. When executed, it SHOULD + * return a new instance; that instance should always return true for + * isRegenerated(). + * + * An example of where this WOULD NOT return a new instance is within the + * shipped LazySession, where instead it would return itself, after + * internally re-setting the proxied session. + */ + public function regenerate(): SessionInterface; + + /** + * Method to determine if the session was regenerated; should return + * true if the instance was produced via regenerate(). + */ + public function isRegenerated() : bool; +} +``` + +The default implementation, and the one you'll most likely interact with, is +`Mezzio\Session\Session`. + +Since version 1.2.0, we provide `Mezzio\Session\SessionCookiePersistenceInterface`: + +```php +namespace Mezzio\Session; + +/** + * Allow marking session cookies as persistent. + * + * It can be useful to mark a session as persistent: e.g., for a "Remember Me" + * feature when logging a user into your system. PHP provides this capability + * via ext-session with the $lifetime argument to session_set_cookie_params() + * as well as by the session.cookie_lifetime INI setting. The latter will set + * the value for all session cookies sent (or until the value is changed via + * an ini_set() call), while the former will only affect cookies created during + * the current script lifetime. + * + * Persistence engines may, of course, allow setting a global lifetime. This + * interface allows developers to set the lifetime programmatically. Persistence + * implementations are encouraged to use the value to set the cookie lifetime + * when creating and returning a cookie. Additionally, to ensure the cookie + * lifetime originally requested is honored when a session is regenerated, we + * recommend persistence engines to store the TTL in the session data itself, + * so that it can be re-sent in such scenarios. + */ +interface SessionCookiePersistenceInterface +{ + const SESSION_LIFETIME_KEY = '__SESSION_TTL__'; + + /** + * Define how long the session cookie should live. + * + * Use this value to detail to the session persistence engine how long the + * session cookie should live. + * + * This value could be passed as the $lifetime value of + * session_set_cookie_params(), or used to create an Expires or Max-Age + * parameter for a session cookie. + * + * Since cookie lifetime is communicated by the server to the client, and + * not vice versa, the value should likely be persisted in the session + * itself, to ensure that session regeneration uses the same value. We + * recommend using the SESSION_LIFETIME_KEY value to communicate this. + * + * @param int $duration Number of seconds the cookie should persist for. + */ + public function persistSessionFor(int $duration) : void; + + /** + * Determine how long the session cookie should live. + * + * Generally, this will return the value provided to persistFor(). + * + * If that method has not been called, the value can return one of the + * following: + * + * - 0 or a negative value, to indicate the cookie should be treated as a + * session cookie, and expire when the window is closed. This should be + * the default behavior. + * - If persistFor() was provided during session creation or anytime later, + * the persistence engine should pull the TTL value from the session itself + * and return it here. Typically, this value should be communicated via + * the SESSION_LIFETIME_KEY value of the session. + */ + public function getSessionLifetime() : int; +} +``` + +`Mezzio\Session\Session` and `Mezzio\Session\LazySession` both +implement each of the interfaces listed above. `Session` accepts an optional +identifier to its constructor, and will use the value of the +`SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY` in the provided data +to seed the session cookie lifetime, if present. + +## Usage + +Session containers will typically be passed to your middleware using the +[SessionMiddleware](middleware.md), via the +`Mezzio\Session\SessionMiddleware::SESSION_ATTRIBUTE` ("session") or the +`Mezzio\Session\SessionInterface::class` ("Mezzio\Session\SessionInterface"; +available since version 1.4.0) request attribute. + +Once you have the container, you can check for data: + +```php +if ($session->has('user')) { +} +``` + +and retrieve it: + +```php +$user = $session->get('user'); +``` + +You can combine those operations, by passing a default value as a second +argument to the `get()` method: + +```php +$user = $session->get('user', new GuestUser()); +``` + +If a datum is no longer relevant in the session, `unset()` it: + +```php +$session->unset('user'); +``` + +If none of the data is relevant, `clear()` the session: + +```php +$session->clear(); +``` + +### Persistent Sessions + +- Since 1.2.0 + +You can hint to the session persistence engine how long the session should +persist: + +```php +$session->persistSessionFor(60 * 60 * 24 * 7); // persist for 7 days +``` + +To make the session expire when the browser session is terminated (default +behavior), use zero or a negative integer: + +```php +$session->persistSessionFor(0); // expire data after session is over +``` + +## Lazy Sessions + +This package provides another implementation of `SessionInterface` via +`Mezzio\Session\LazySession`. This implementation does the following: + +- It composes a [persistence](persistence.md) instance, along with the current + request. +- On _first access_ (e.g., `get()`, `set()`, etc.), it uses the composed + persistence and request instances to generate the _actual_ session container. + All methods then _proxy_ to this container. + +This approach helps delay any I/O or network operations, and/or +deserialization, until they are actually needed. + +The shipped [SessionMiddleware](middleware.md) produces a `LazySession`. + +## Session Regeneration + +Some application events benefit from _session regeneration_. In particular, +after a user has successfully logged in or out, you will generally want to +regenerate the session in order to prevent session fixation and the attack +vectors it invites. + +In those situations, call `regenerate()`: + +```php +$newSession = $session->regenerate(); +``` + +The interface indicates that a new instance _should_ be returned. However, in +the default usage, you will have a `LazySession` instance (as described above), +which _decorates_ the underlying session storage. This is done for two reasons: + +- First, the stated reasons of preventing the need to deserialize data and/or + perform I/O access until the last moment. +- Second, to ensure that the `SessionMiddleware` _always has a pointer to the + session_. + +This latter is what allows you to regenerate the session in middleware nested +deep in your application, but still have the data persisted correctly. diff --git a/mkdocs.yml b/mkdocs.yml index 0a71782..e10403b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,6 +2,13 @@ docs_dir: docs/book site_dir: docs/html nav: - Home: index.md + - v2: + - Introduction: v2/intro.md + - Installation: v2/installation.md + - Usage: + - "Session Containers": v2/session.md + - "Session Persistence": v2/persistence.md + - "Session Middleware": v2/middleware.md - v1: - Introduction: v1/intro.md - Installation: v1/installation.md @@ -16,14 +23,15 @@ extra: project: Mezzio installation: config_provider_class: 'Mezzio\Session\ConfigProvider' - current_version: v1 + current_version: v2 versions: + - v2 - v1 plugins: - redirects: redirect_maps: - intro.md: v1/intro.md - installation.md: v1/installation.md - session.md: v1/session.md - persistence.md: v1/persistence.md - middleware.md: v1/middleware.md + intro.md: v2/intro.md + installation.md: v2/installation.md + session.md: v2/session.md + persistence.md: v2/persistence.md + middleware.md: v2/middleware.md diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 9c1b45b..6ef2312 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -5,20 +5,12 @@ generateCacheHeaders - - - LazySession - - __construct - - Session - null|bool|int|float|string|array @@ -26,11 +18,6 @@ json_decode(json_encode($value, JSON_PRESERVE_ZERO_FRACTION), true) - - - - - provideCacheHeaderValues diff --git a/src/LazySession.php b/src/LazySession.php index da327ba..b522b23 100644 --- a/src/LazySession.php +++ b/src/LazySession.php @@ -19,7 +19,6 @@ */ final class LazySession implements SessionCookiePersistenceInterface, - SessionIdentifierAwareInterface, SessionInterface, InitializeSessionIdInterface { @@ -110,10 +109,7 @@ public function hasChanged(): bool */ public function getId(): string { - $proxiedSession = $this->getProxiedSession(); - return $proxiedSession instanceof SessionIdentifierAwareInterface - ? $proxiedSession->getId() - : ''; + return $this->getProxiedSession()->getId(); } /** diff --git a/src/Session.php b/src/Session.php index 2244784..564a553 100644 --- a/src/Session.php +++ b/src/Session.php @@ -13,7 +13,6 @@ class Session implements SessionCookiePersistenceInterface, - SessionIdentifierAwareInterface, SessionInterface { /** diff --git a/src/SessionIdentifierAwareInterface.php b/src/SessionIdentifierAwareInterface.php deleted file mode 100644 index a8632a0..0000000 --- a/src/SessionIdentifierAwareInterface.php +++ /dev/null @@ -1,27 +0,0 @@ -assertSame($expected, $session->get('foo')); } - public function testImplementsSessionIdentifierAwareInterface(): void - { - $session = new Session([]); - $this->assertInstanceOf(SessionIdentifierAwareInterface::class, $session); - } - public function testGetIdReturnsEmptyStringIfNoIdentifierProvidedToConstructor(): void { $session = new Session([]);