seba1rx / sessionadmin
PHP session management with security hardening, hijacking detection, and URL authorization for MPA and SPA applications
Requires
- php: >=8.1
Requires (Dev)
- phpunit/phpunit: ^13.0
README
PHP session management library with security hardening and URL authorization.
composer require seba1rx/sessionadmin
Features
Session security
- Named sessions with configurable cookie parameters
- Hijacking detection: IP prefix + User-Agent fingerprint verified on every request
- Proxy-aware IP detection (reads
X-Forwarded-Forand equivalent headers) - Session destruction when a request arrives after the configured lifetime
- Session ID regenerated on login and randomly (~3% of requests) to resist fixation
URL authorization (MPA)
- Define an
$allowedUrlslist; guests are redirected toindex.phpon any unlisted page - Expandable per user role or profile
- Disable entirely for SPA apps (
$appIsSpa = true, the default)
Quick start
SessionAdmin is abstract with no abstract methods. Extend it and define a constructor:
// App/MySession.php namespace App; use Seba1rx\SessionAdmin\SessionAdmin; class MySession extends SessionAdmin { public function __construct() { $this->sessionName = 'my_app'; $this->sessionLifetime = 3600; // seconds $this->keys = ['theme' => 'light']; // pre-seeded session keys } }
Then on every entry point, before any output:
require 'vendor/autoload.php'; $session = new App\MySession(); $session->useAuthorization = false; // true for MPA URL enforcement $session->activateSession(); // replaces session_start() // On login: $session->createUserSession($userId); // On logout: $session->terminate(); // Auth check: if (!empty($_SESSION['sessionadmin']['isUser'])) { // authenticated }
Public API
| Method | Description |
|---|---|
activateSession() |
Starts or resumes the session; runs all security checks |
createUserSession(mixed $id) |
Marks session as authenticated, regenerates session ID |
terminate() |
Destroys session, reinitialises as guest, redirects to index.php (MPA) |
setSessionHandler(\SessionHandlerInterface $handler) |
Plug in a custom storage backend; call before activateSession() |
setTabHandler(TabHandlerInterface $handler) |
Inject a tab handler (e.g. seba1rx/tabmanager); call before activateSession() |
The full class is documented via docblocks — your IDE will surface every property and its purpose.
Session data written to $_SESSION['sessionadmin']
| Key | Present | Description |
|---|---|---|
appType |
Always | 'SPA' or 'MPA' — reflects the $appIsSpa flag |
isUser |
Always | true when authenticated, false for guests |
id_user |
After login | Value passed to createUserSession() |
msg |
Always | Human-readable state label |
uniqueId |
Always | 12-char hex token, stable for the session lifetime |
ipPrefix |
Always | First N octets of the client IP (hijacking detection) |
userAgent |
Always | User-Agent string (hijacking detection) |
time_atRequest |
Always | Unix timestamp of the last request |
time_sinceLastRequest |
Always | Seconds elapsed since the previous request |
allowedUrl |
MPA only | Copy of $allowedUrls used for URL authorization |
urlIsAllowedToLoad |
MPA only | true when the current URL is in the allow-list |
allowedUrl and urlIsAllowedToLoad are omitted entirely in SPA mode — they only make sense when URL authorization is active.
Custom session storage
By default the package uses PHP's native file-based session storage. Pass any SessionHandlerInterface implementation to setSessionHandler() before calling activateSession() to swap the backend:
$session = new App\MySession(); $session->setSessionHandler(new RedisSessionHandler($redis)); $session->activateSession();
Any PSR-compatible or custom handler works — Redis, database, encrypted file store, etc. The handler must be set before activateSession() because PHP applies the handler prior to calling session_start().
Tab isolation (optional)
Per-browser-tab session isolation is provided by the companion package seba1rx/tabmanager, which implements TabHandlerInterface.
composer require seba1rx/tabmanager
Inject it before calling activateSession() using SessionAdminBridge — the integration class shipped with tabmanager:
use Seba1rx\TabManager\Bridge\SessionAdminBridge; $session = new App\MySession(); $session->setTabHandler(new SessionAdminBridge()); $session->autoCleanupTabs = 30; // optional: remove tabs inactive for > 30 s $session->activateSession(); // After the JS client has registered the tab: $session->tabHandler->set('cart', ['apple' => 3]); $cart = $session->tabHandler->get('cart'); $ready = $session->tabHandler->isTabIndexed(); // false until JS registers the tab
SessionAdminBridge extends TabManager but does not call session_start() in its constructor — activateSession() owns the session lifecycle and configures the session name and cookie parameters before the session starts. Both packages write to distinct keys in $_SESSION (sessionadmin vs tabmanager) and do not interfere with each other.
Tab session loss (autoCleanupTabs + tabmanager:session-lost)
When $autoCleanupTabs is set, SessionAdmin prunes inactive tabs on every activateSession() call. If the browser suspends a tab (Chrome Memory Saver, OS memory pressure), the JS heartbeat pauses — and the tab may be pruned while invisible. When the user returns, tabmanager's JS client checks /tabmanager/tab-status. If the tab is no longer indexed, it fires a tabmanager:session-lost event on document and stops the heartbeat. Listen for this event to show a warning or prompt the user to reload:
document.addEventListener('tabmanager:session-lost', () => { // Tab data was pruned by autoCleanupTabs while the tab was suspended. // Show a warning and let the user decide whether to reload. showSessionLostBanner(); });
The event carries event.detail.tabId with the UUID of the lost tab.
Contracts (interfaces)
The package ships two interfaces under Seba1rx\SessionAdmin\Contracts:
| Interface | Role | Key methods |
|---|---|---|
SessionInterface |
Implemented by SessionAdmin |
activateSession(), createUserSession(), terminate() |
TabHandlerInterface |
Implemented by seba1rx/tabmanager |
set(), get(), isTabIndexed(), cleanupInactiveTabs(), … |
TabHandlerInterface defines the full tab lifecycle contract. Any class implementing it can be injected via setTabHandler() — SessionAdmin never depends on the concrete TabManager class.
Example — mock session in tests:
$mockSession = $this->createMock(SessionInterface::class); $mockSession->expects($this->once())->method('activateSession');
Example — mock tab handler in tests:
$mockTabs = $this->createMock(TabHandlerInterface::class); $mockTabs->method('get')->with('cart')->willReturn(['apple' => 3]); $session->setTabHandler($mockTabs);
Demos
| Demo | Description |
|---|---|
demo/basic/ |
Minimal login/logout — the simplest possible implementation |
demo/mpa/ |
Multi-page app with URL authorization and $allowedUrls |
demo/spa/ |
Single-page app, SPA mode, AJAX login |
demo/tabmanager/ |
SessionAdmin + TabManager integration — shared session, per-tab data isolation |
Each demo is self-contained with its own composer.json.
Running a demo locally
- Install dependencies for the chosen demo:
cd demo/basic
composer install
- Start PHP's built-in web server from the demo directory:
php -S localhost:8000
- Open your browser and navigate to:
http://localhost:8000
The built-in server serves
index.phpby default. Change the port if8000is already in use (php -S localhost:8080).