📦 Marketplace⭐ GitHub
Auth & Securityv2.0

Session

Secure, driver-based HTTP session management for the MonkeysLegion framework. Ground-up rebuild for PHP 8.4 with property hooks, typed Bag architecture, and zero hard dependencies beyond PSR-7/15.

Features

FeatureStatus
Multi-Driver StorageFile, Database (via ConnectionManagerInterface), Redis — switchable via DriverFactory
Bag ArchitectureAttributeBag, FlashBag, MetadataBag — each implements SessionBagInterface
Atomic LockingPer-session lock() / unlock() on every driver to prevent race conditions
AES-256-GCM EncryptionOptional payload encryption with key-ring rotation support
CSRF ProtectionAuto-generated tokens + VerifyCsrfToken PSR-15 middleware
Flash DataOne-hop flash messages with reflash(), keep(), and now()
Session Fixation Preventionregenerate() and invalidate() for safe login flows
Dot Notation AccessNested get() / set() via user.profile.name style keys
Service ProviderSessionServiceProvider for PSR-11 DI registration
PHP 8.4 NativeProperty hooks ($id, $isStarted, $name), readonly constructors, typed constants

Requirements

  • PHP 8.4 or higher
  • psr/http-message ^2.0
  • psr/http-server-middleware ^1.0
  • psr/http-server-handler ^1.0

Installation

composer require monkeyscloud/monkeyslegion-session

Architecture

monkeyslegion-session/
├── config/
│   ├── session.mlc                    # MLC configuration format
│   └── session.php                    # PHP configuration format
├── src/
│   ├── Bags/
│   │   ├── AttributeBag.php           # Session attribute storage with dot notation
│   │   ├── FlashBag.php               # One-hop flash message container
│   │   └── MetadataBag.php            # Timestamps and usage tracking
│   ├── Cli/
│   │   └── Command/
│   │       └── ConfigPublisher.php    # CLI command to publish config files
│   ├── Contracts/
│   │   ├── DataHandlerInterface.php   # Serialization / encryption contract
│   │   ├── SessionBagInterface.php    # Bag contract (initialize, clear, getStorageKey)
│   │   ├── SessionDriverInterface.php # Storage driver contract (with lock/unlock)
│   │   └── SessionInterface.php       # Session manager API contract
│   ├── Drivers/
│   │   ├── DatabaseDriver.php         # Database storage via ConnectionManagerInterface
│   │   ├── FileDriver.php             # Filesystem storage with flock() locking
│   │   └── RedisDriver.php            # Redis storage with EXPIRE-based GC
│   ├── Exceptions/
│   │   ├── SessionException.php       # Named constructors for all error cases
│   │   └── SessionLockException.php   # Lock acquisition failure
│   ├── Factory/
│   │   └── DriverFactory.php          # Creates drivers from config arrays
│   ├── Middleware/
│   │   ├── SessionMiddleware.php      # PSR-15 session lifecycle middleware
│   │   └── VerifyCsrfToken.php        # CSRF token validation middleware
│   ├── EncryptedSerializer.php        # AES-256-GCM with key-ring rotation
│   ├── NativeSerializer.php           # Standard PHP serialize/unserialize
│   ├── SessionBag.php                 # Legacy data container (attributes + flash)
│   ├── SessionManager.php             # Session orchestrator (implements SessionInterface)
│   └── SessionServiceProvider.php     # PSR-11 DI registration
└── tests/

Quick Start

1. Register the Service Provider

The SessionServiceProvider binds SessionDriverInterfaceFileDriver by default. Register it with the DI container:

use MonkeysLegion\Session\SessionServiceProvider;

// In your application bootstrap
$provider = new SessionServiceProvider();
$provider->register($containerBuilder);

To swap drivers, override the binding in your container definitions:

use MonkeysLegion\Session\Contracts\SessionDriverInterface;
use MonkeysLegion\Session\Drivers\RedisDriver;

$builder->bind(SessionDriverInterface::class, RedisDriver::class);

2. Add Middleware

Register SessionMiddleware in your PSR-15 pipeline to enable automatic session lifecycle management:

use MonkeysLegion\Session\Middleware\SessionMiddleware;

$pipeline->add(SessionMiddleware::class);

For CSRF protection on state-changing requests, add the VerifyCsrfToken middleware after the session middleware:

use MonkeysLegion\Session\Middleware\VerifyCsrfToken;

$pipeline->add(VerifyCsrfToken::class);

3. Use in Controllers

The session is injected as a request attribute by the middleware:

class ProfileController
{
    public function show(ServerRequestInterface $request): ResponseInterface
    {
        /** @var \MonkeysLegion\Session\SessionManager $session */
        $session = $request->getAttribute('session');

        // Read data (dot notation supported)
        $name = $session->get('user.profile.name', 'Guest');

        // Write data
        $session->set('last_visited', time());

        // Flash a one-time message
        $session->flash('status', 'Welcome back!');

        // ...
    }
}

Session Manager API

The SessionManager implements SessionInterface and is the primary class your application interacts with.

Property Hooks (PHP 8.4)

// Read-only session ID (throws SessionException if set while started)
$manager->id;               // string

// Check if session is active
$manager->isStarted;        // bool

Lifecycle Methods

MethodReturnsDescription
start(?string $id)boolResume an existing session or start a new one. Acquires a driver lock, reads storage, initializes the SessionBag, and auto-generates a CSRF token if missing.
save()boolSerialize attributes, write to the driver, and release the lock.
regenerate(bool $destroy)boolChange the session ID (prevents fixation). Optionally destroy old data.
invalidate()boolFlush all data and regenerate the ID (full session reset).
getName()stringGet the session cookie name (default: ml_session).
getId()stringAlias for the $id property hook.
isStarted()boolAlias for the $isStarted property hook.

Data Access

MethodDescription
get(string $key, mixed $default = null)Retrieve data (dot notation supported).
set(string $key, mixed $value)Store data in the session.
has(string $key)Check if a key exists.
forget(string $key)Remove a key.
pull(string $key, mixed $default = null)Get a value and immediately delete it.
all()Return all session attributes.

Flash Data

MethodDescription
flash(string $key, mixed $value)Store data for the next request only.
getFlash(string $key, mixed $default)Retrieve flash data from the previous request.
reflash()Keep all flash data for one more request.
keep(string ...$keys)Keep specific flash keys for one more request.
now(string $key, mixed $value)Flash data available immediately, expires at end of current request.

Security

MethodDescription
token()Get the current CSRF token.
regenerateToken()Regenerate the CSRF token (80 hex chars via random_bytes).
setIpAddress(?string $ip)Set the user's IP for fingerprinting.
setUserAgent(?string $ua)Set the user's browser string for fingerprinting.
setUserId(string|int|null $id)Associate a user ID with the session.
setRequestInfo(?string $ip, ?string $ua)Convenience: set IP and User-Agent at once.

Bag Architecture

Version 2 introduces a segmented Bag system that implements SessionBagInterface. Each bag manages a distinct concern:

SessionBagInterface

interface SessionBagInterface
{
    public string $name { get; }                     // Bag identifier (property hook)
    public function initialize(array &$array): void; // Initialize with reference to storage
    public function getStorageKey(): string;          // Storage key prefix (e.g. _attributes)
    public function clear(): array;                   // Clear and return previous contents
}

AttributeBag

Persistent key/value storage with dot notation:

$bag = new AttributeBag();
$bag->set('user.preferences.theme', 'dark');
$bag->get('user.preferences.theme');  // 'dark'
$bag->has('user.preferences');        // true
$bag->pull('temp_key');               // get + forget
$bag->forget('user.preferences.theme');

FlashBag

One-hop data with fine-grained retention:

$flash = new FlashBag();

// Set flash for next request
$flash->set('success', 'Profile updated!');

// Set flash available now, gone after this request
$flash->now('error', 'Something failed.');

// Keep specific keys alive for one more hop
$flash->keep('success', 'warning');

// Keep everything alive
$flash->reflash();

// Auto-cleanup: call before save
$flash->clearOldData();

MetadataBag

Session timestamps and usage tracking:

$meta = new MetadataBag();

$meta->getCreatedAt();     // Unix timestamp of session creation
$meta->getLastUsedAt();    // Last activity timestamp

// Throttle DB writes: only update last_used_at every N seconds
$meta->setUpdateThreshold(300);
$meta->stampNew();         // Conditionally updates based on threshold

Storage Drivers

All drivers implement SessionDriverInterface with atomic locking:

SessionDriverInterface

MethodArgumentsReturnsDescription
open$path, $nameboolInitialize the storage resource.
closeboolClose the storage resource.
read$id?arrayRetrieve session data (payload + metadata) by ID.
write$id, $payload, $metadataboolSave serialized data and metadata.
destroy$idboolDelete the session from storage.
gc$maxLifetimeint|falseGarbage collect sessions older than N seconds.
lock$id, $timeout = 30boolAcquire exclusive lock on the session.
unlock$idboolRelease the session lock.

File Driver

Uses flock() for atomic locking. Sessions stored as individual files:

use MonkeysLegion\Session\Drivers\FileDriver;

$driver = new FileDriver(
    path: '/var/sessions',
    ttl: 7200
);

Database Driver

Stores sessions in a SQL table via ConnectionManagerInterface:

use MonkeysLegion\Session\Drivers\DatabaseDriver;

$driver = new DatabaseDriver($connectionManager, [
    'table'    => 'sessions',
    'lifetime' => 7200,
]);

Migration

CREATE TABLE sessions (
    session_id    VARCHAR(255) PRIMARY KEY NOT NULL,
    payload       TEXT,
    flash_data    TEXT,
    created_at    INTEGER NOT NULL,
    last_activity INTEGER NOT NULL,
    expiration    INTEGER NOT NULL,
    user_id       INTEGER NULL,
    ip_address    VARCHAR(45) NULL,
    user_agent    TEXT NULL
);

Redis Driver

Uses EXPIRE for automatic garbage collection:

use MonkeysLegion\Session\Drivers\RedisDriver;

$driver = new RedisDriver(
    redis: $redisInstance,
    prefix: 'session:',
    ttl: 7200
);

Driver Factory

Create drivers from configuration arrays dynamically:

use MonkeysLegion\Session\Factory\DriverFactory;

$factory = new DriverFactory();
$driver  = $factory->make('file', ['path' => '/var/sessions', 'lifetime' => 7200]);
$driver  = $factory->make('redis', ['redis' => $redis, 'prefix' => 'sess:', 'lifetime' => 3600]);

Payload Encryption

Session data can optionally be encrypted at rest using AES-256-GCM with key-ring rotation support.

NativeSerializer (Default)

Standard PHP serialize() / unserialize():

use MonkeysLegion\Session\NativeSerializer;

$handler = new NativeSerializer();

EncryptedSerializer

Wraps any DataHandlerInterface with AES-256-GCM encryption. Supports multiple keys for zero-downtime key rotation:

use MonkeysLegion\Session\EncryptedSerializer;
use MonkeysLegion\Session\NativeSerializer;

$handler = new EncryptedSerializer(
    serializer: new NativeSerializer(),
    keys: [
        'v2' => 'new-256-bit-key-here',   // Current key (used for encryption)
        'v1' => 'old-256-bit-key-here',    // Legacy key (used for decryption fallback)
    ]
);

// Inject into the SessionManager
$manager = new SessionManager($driver, $handler);

Key rotation works transparently: new writes always use the first key in the ring. Reads attempt the tagged key first, then fall back through all keys. Remove old keys once all sessions have been re-encrypted.


CSRF Protection

The session middleware auto-generates a CSRF token (_token) when a session starts.

Rendering in Templates

<form method="POST" action="/profile">
  <input type="hidden" name="_csrf" value="{{ session.token() }}" />
  <!-- ... -->
</form>

VerifyCsrfToken Middleware

The middleware checks tokens on all non-read methods (POST, PUT, PATCH, DELETE):

  • _csrf field in the parsed body
  • X-CSRF-TOKEN header
  • X-XSRF-TOKEN header (fallback)

Read methods (GET, HEAD, OPTIONS) pass through automatically.

use MonkeysLegion\Session\Middleware\VerifyCsrfToken;

// Register after SessionMiddleware
$pipeline->add(VerifyCsrfToken::class);

Configuration

Publish the config file using the CLI command:

php mlc session:publish

MLC Format (config/session.mlc)

session {
    default env(SESSION_DRIVER, 'file')

    drivers {
        file {
            path => env(SESSION_FILE_PATH, base_path('var/sessions'))
            lifetime => env(SESSION_LIFETIME, 7200)
        }
        database {
            table => env(SESSION_TABLE, 'sessions')
            lifetime => env(SESSION_LIFETIME, 7200)
        }
        redis {
            connection => env(REDIS_SESSION_CONNECTION, 'default')
            lifetime => env(SESSION_LIFETIME, 7200)
        }
    }

    cookie_name => env(SESSION_COOKIE_NAME, 'ml_session')
    cookie_lifetime => env(SESSION_COOKIE_LIFETIME, 7200)
    cookie_path => env(SESSION_COOKIE_PATH, '/')
    cookie_domain => env(SESSION_COOKIE_DOMAIN, '')
    cookie_secure => env(SESSION_COOKIE_SECURE, true)
    cookie_httponly => env(SESSION_COOKIE_HTTPONLY, true)
    cookie_samesite => env(SESSION_COOKIE_SAMESITE, 'Lax')

    encrypt => env(SESSION_ENCRYPT, false)

    keys {
        main_key => env(APP_KEY, null)
    }
}

PHP Format (config/session.php)

return [
    'session' => [
        'default' => $_ENV['SESSION_DRIVER'] ?? 'file',
        'drivers' => [
            'file' => [
                'path'     => $_ENV['SESSION_FILE_PATH'] ?? base_path('var/sessions'),
                'lifetime' => (int) ($_ENV['SESSION_LIFETIME'] ?? 7200),
            ],
            'database' => [
                'table'    => $_ENV['SESSION_TABLE'] ?? 'sessions',
                'lifetime' => (int) ($_ENV['SESSION_LIFETIME'] ?? 7200),
            ],
            'redis' => [
                'connection' => $_ENV['REDIS_SESSION_CONNECTION'] ?? 'default',
                'lifetime'   => (int) ($_ENV['SESSION_LIFETIME'] ?? 7200),
            ],
        ],
        'cookie_name'     => $_ENV['SESSION_COOKIE_NAME'] ?? 'ml_session',
        'cookie_lifetime' => (int) ($_ENV['SESSION_COOKIE_LIFETIME'] ?? 7200),
        'cookie_path'     => $_ENV['SESSION_COOKIE_PATH'] ?? '/',
        'cookie_domain'   => $_ENV['SESSION_COOKIE_DOMAIN'] ?? '',
        'cookie_secure'   => (bool) ($_ENV['SESSION_COOKIE_SECURE'] ?? true),
        'cookie_httponly'  => (bool) ($_ENV['SESSION_COOKIE_HTTPONLY'] ?? true),
        'cookie_samesite'  => $_ENV['SESSION_COOKIE_SAMESITE'] ?? 'Lax',
        'encrypt'          => (bool) ($_ENV['SESSION_ENCRYPT'] ?? false),
        'keys' => [
            'main_key' => $_ENV['APP_KEY'] ?? null,
        ],
    ],
];

Middleware Lifecycle

The SessionMiddleware manages the full request/response lifecycle:

PhaseActionDetail
1. ExtractgetCookieParams()Read the session ID from the configured cookie name.
2. Startmanager->start()Lock → Read → Initialize Bag → Generate CSRF token if missing.
3. MetadatapopulateMetadata()Extract IP (REMOTE_ADDR / X-Forwarded-For) and User-Agent.
4. InjectwithAttribute('session')Attach the SessionManager to the PSR-7 request.
5. Processhandler->handle()Application logic runs (routes, controllers).
6. Commitmanager->save()Serialize → Write → Unlock (in a finally block for safety).
7. CookiewithAddedHeader()Set the Set-Cookie header with secure defaults.

Security Posture

  • Atomic locking — prevents concurrent request race conditions via flock() / Redis SETNX / row-level locks
  • CSPRNG session IDsrandom_bytes(20) (40 hex chars) for all session identifiers
  • CSRF token generationrandom_bytes(40) (80 hex chars) for CSRF tokens
  • Session fixation preventionregenerate(destroy: true) on login
  • AES-256-GCM encryption — authenticated encryption with key-ring rotation
  • IP & User-Agent fingerprinting — stored per-session for anomaly detection
  • Timing-safe comparisonshash_equals for all CSRF token checks
  • Graceful lock releasefinally block ensures unlock even on exceptions

Error Handling

The SessionException class provides named constructors for clear, debuggable error messages:

SessionException::alreadyStarted();            // "Session has already been started."
SessionException::notStarted();                // "Session has not been started yet."
SessionException::invalidId($id);              // "Invalid session ID: ..."
SessionException::serializationFailed($msg);   // "Failed to serialize session data."
SessionException::deserializationFailed($msg); // "Failed to deserialize session data."
SessionException::driverFailed($op, $msg);     // "Session driver operation '...' failed."
SessionException::securityValidationFailed($r);// "Session security validation failed: ..."
SessionException::expired();                   // "Session has expired."

Testing

composer test
composer phpstan

License

MIT © MonkeysCloud