Deterministic session handling. Minimal overhead. Security-first defaults.
The CitOmni Session Service provides a lean wrapper around PHP's native session handling.
It is a first-class service within citomni/http and exposes a small, predictable API for session lifecycle, session state, session cookie policy, optional ID rotation, and optional fingerprint binding.
This document defines the behavioral contract, configuration model, runtime semantics, and operational guarantees of the Session service.
The intent is not to describe PHP sessions in general, but to document the exact behavior of the CitOmni implementation.
- Service:
CitOmni\Http\Service\Session - Package:
citomni/http - Runtime availability: HTTP runtime
- PHP version: ≥ 8.2
- Audience: Framework developers and application integrators
- Status: Stable
- Document type: Reference
The CitOmni Session Service manages PHP sessions using a lazy-start, configuration-driven, and security-aware wrapper.
Key characteristics:
- Lazy autostart
- Hardened runtime defaults
- Deterministic cookie policy resolution
- Optional session ID rotation
- Optional fingerprint binding
- Explicit failure semantics
- Minimal overhead
The service intentionally avoids framework magic and external abstractions.
It is designed to remain cheap, predictable, and operationally safe.
The session service is registered as a normal CitOmni service and extends BaseService.
It is resolved through the application service container:
$this->app->sessionExample:
$this->app->session->start();
$this->app->session->set('user_id', $userId);Service initialization occurs lazily when the service is first resolved.
The service performs no eager boot work beyond its normal construction path.
The service primarily reads configuration from:
$this->app->cfg->sessionIt also uses fallback values from:
$this->app->cfg->cookie
$this->app->cfg->httpAll configuration values are optional.
If the relevant configuration nodes are absent, built-in defaults are used.
| Key | Type | Description |
|---|---|---|
use_strict_mode |
bool | Enables PHP strict session mode |
use_only_cookies |
bool | Restricts session propagation to cookies only |
lazy_write |
bool | Enables PHP lazy session write behavior |
gc_maxlifetime |
int | Session garbage collection lifetime in seconds |
sid_length |
int | Session ID length for PHP < 8.4 |
sid_bits_per_character |
int | SID bits per character for PHP < 8.4 |
save_path |
string | Session storage directory |
name |
string | Session cookie name |
rotate_interval |
int | Rotation interval in seconds. 0 disables rotation |
fingerprint.bind_user_agent |
bool | Include user agent hash in fingerprint |
fingerprint.bind_ip_octets |
int | Number of IPv4 octets to bind |
fingerprint.bind_ip_blocks |
int | Number of IPv6 blocks to bind |
cookie_secure |
bool | Session-specific override for Secure cookie flag |
cookie_httponly |
bool | Session-specific override for HttpOnly cookie flag |
cookie_samesite |
string | Session-specific override for SameSite |
cookie_path |
string | Session-specific override for cookie path |
cookie_domain |
string | null |
If the session-specific cookie keys are absent, the service falls back to:
| Key | Type | Description |
|---|---|---|
cookie.secure |
bool | Fallback Secure flag |
cookie.httponly |
bool | Fallback HttpOnly flag |
cookie.samesite |
string | Fallback SameSite |
cookie.path |
string | Fallback cookie path |
cookie.domain |
string | null |
If session.cookie_secure and cookie.secure are both absent, Secure is inferred from:
http.base_urlif it starts withhttps://Request::isHttps()when the Request service is available- internal fallback detection using
$_SERVER['HTTPS']or port443
'session' => [
'use_strict_mode' => true,
'use_only_cookies' => true,
'lazy_write' => true,
'gc_maxlifetime' => 1440,
'name' => 'CITOMNISESSID',
'rotate_interval' => 900,
'fingerprint' => [
'bind_user_agent' => true,
'bind_ip_octets' => 2,
'bind_ip_blocks' => 0,
],
'cookie_samesite' => 'Lax',
'cookie_httponly' => true,
],
'cookie' => [
'secure' => true,
'path' => '/',
'domain' => null,
],If not configured:
| Setting | Default |
|---|---|
use_strict_mode |
true |
use_only_cookies |
true |
lazy_write |
true |
gc_maxlifetime |
1440 |
sid_length |
48 for PHP < 8.4 |
sid_bits_per_character |
6 for PHP < 8.4 |
| cookie lifetime | 0 |
cookie httponly |
true |
cookie samesite |
Lax |
cookie path |
/ |
cookie domain |
null |
rotate_interval |
0 |
| fingerprint binding | disabled |
When save_path is configured:
- the path is created if missing
- directory creation is attempted with
0775 - the resulting path is passed to
ini_set('session.save_path', ...)
If directory creation fails silently at the PHP level, later session startup may fail.
The public API is intentionally small.
public function start(): void
public function isActive(): bool
public function id(): ?string
public function get(string $key): mixed
public function set(string $key, mixed $value): void
public function has(string $key): bool
public function remove(string $key): void
public function destroy(bool $forgetCookie = true): void
public function regenerate(bool $deleteOld = true): voidpublic function start(): voidExplicitly starts the session.
- no-op if a session is already active
- otherwise calls the internal startup pipeline
- guarantees that
$_SESSIONis available on return
| Exception | Condition |
|---|---|
RuntimeException |
Headers already sent before session start |
RuntimeException |
session_start() fails |
public function isActive(): boolReturns whether a PHP session is currently active.
- returns
trueonly whensession_status() === PHP_SESSION_ACTIVE - does not start the session
- has no side effects
public function id(): ?stringReturns the current session ID.
- returns
session_id()when a session is active - returns
nullwhen no session is active - does not start the session
public function get(string $key): mixedRetrieves a session value by key.
- lazily starts the session if needed
- returns the stored value when present
- returns
nullwhen absent - does not modify session state
- keys are case-sensitive
- values may be scalar, array, object, or
null
public function set(string $key, mixed $value): voidStores a value in the session.
- lazily starts the session if needed
- writes directly to
$_SESSION[$key] - overwrites any existing value for the same key
- keys are case-sensitive
- values must be serializable by the active PHP session handler
public function has(string $key): boolChecks whether a session key exists.
- lazily starts the session if needed
- uses
array_key_exists() - distinguishes between absent keys and keys explicitly set to
null
public function remove(string $key): voidRemoves a session key.
- lazily starts the session if needed
- calls
unset($_SESSION[$key]) - no-op when the key does not exist
public function destroy(bool $forgetCookie = true): voidDestroys the active session.
The destroy operation performs the following steps:
- ensure a session is active
- call
session_unset() - call
session_destroy() - call
session_write_close() - reset local
$_SESSIONto an empty array - optionally expire the client session cookie
When $forgetCookie = true:
- the session cookie is expired using the active cookie parameters
- path, domain, secure, httponly, and samesite are preserved from the active runtime values
- the expiry timestamp is set to a time in the past
- this method is intended for logout and other explicit session teardown flows
- a new session may later be created through
start()or any lazy read/write call
public function regenerate(bool $deleteOld = true): voidRegenerates the session ID.
- requires an active session
- calls
session_regenerate_id($deleteOld) - stores the current timestamp in
$_SESSION['_sess_rotated_at']
- after login
- after privilege elevation
- after other security-sensitive state transitions
| Exception | Condition |
|---|---|
RuntimeException |
No active session exists |
The service uses a lazy startup gate through its internal ensureStarted() method.
The startup sequence is:
- return immediately if the session is already active
- verify that headers have not already been sent
- initialize INI directives and cookie parameters once
- call
session_start() - apply optional fingerprint binding
- apply optional rotation checks
This lifecycle is deterministic and shared by all lazy public APIs.
Calling get(), set(), has(), or remove() can trigger session startup.
Code that must avoid sending session headers late in the response should call start() explicitly.
The service resolves session cookie flags deterministically.
Resolution order for Secure:
session.cookie_securecookie.secure- HTTPS inference from
http.base_url Request::isHttps()- internal server-variable fallback
Resolution order for the remaining cookie flags:
session.cookie_httponly->cookie.httponly-> built-in defaultsession.cookie_samesite->cookie.samesite-> built-in defaultsession.cookie_path->cookie.path-> built-in defaultsession.cookie_domain->cookie.domain-> built-in default
SameSite=None requires Secure=true.
If SameSite=None is resolved while Secure is false, the service throws a RuntimeException.
This is a hard invariant.
The Session service includes two optional security mechanisms beyond PHP's native runtime behavior.
Rotation is controlled by:
session.rotate_intervalBehavior:
0disables interval-based rotation- when enabled, the service stores the last rotation timestamp in:
$_SESSION['_sess_rotated_at']- if the configured interval has elapsed, the session ID is regenerated
This mechanism is supplementary.
Application code should still explicitly call regenerate() after login or privilege changes.
Fingerprint binding is controlled by:
session.fingerprint.bind_user_agent
session.fingerprint.bind_ip_octets
session.fingerprint.bind_ip_blocksBehavior:
- the service derives a compact fingerprint from enabled components
- the fingerprint is stored in:
$_SESSION['_sess_fpr']-
on mismatch, the service:
- destroys the current session
- starts a fresh session
- stores the new fingerprint
When enabled, the service stores a SHA-1 hash of the current user agent string.
When enabled, the service binds to the leading IPv4 octets.
Examples:
| Config | Bound prefix |
|---|---|
1 |
203 |
2 |
203.0 |
3 |
203.0.113 |
4 |
full IPv4 address |
When enabled, the service binds to the leading IPv6 16-bit blocks after normalization.
- fingerprint binding is best-effort
- aggressive IP binding may cause false mismatches in mobile or proxied environments
- all binding options are disabled by default
The service reserves the following internal session keys:
| Key | Purpose |
|---|---|
'_sess_rotated_at' |
Timestamp of last session ID rotation |
'_sess_fpr' |
Stored session fingerprint |
Applications should treat these keys as reserved implementation details.
They should not be repurposed for application-level data.
The service follows a fail-fast model.
- headers were already sent before session startup
session_start()failsSameSite=Noneis configured withoutSecure=trueregenerate()is called without an active session
- a requested key is absent
- a removed key does not exist
has()checks a missing keyisActive()is called before startupid()is called before startup
This keeps the public API predictable for normal application flows.
The service is intentionally conservative.
Key runtime guarantees:
- startup settings are initialized once per request or process
- startup is idempotent
- no duplicate startup work occurs after activation
- session state access remains direct and thin
- session policy derives from explicit configuration and deterministic fallbacks
The service does not introduce additional abstraction layers over the underlying session store.
It remains a thin wrapper around PHP session primitives.
The Session service is designed for low overhead.
Key performance properties:
- no eager session startup
- no heavy boot logic
- no per-call config rebuild beyond normal service access
- no extra serialization layer beyond the native PHP session handler
- no work performed by
isActive()orid() - optional security checks are cheap and fully conditional
The main cost remains the native PHP session handler and the chosen storage backend.
The service always sets cookie lifetime to 0.
This means the session cookie is a browser-session cookie.
There is no service-level configuration key for overriding lifetime.
The service is intended for HTTP runtime usage.
It should not be treated as a general CLI state persistence mechanism.
The service can consult the Request service for HTTPS detection when available.
If the Request service is absent, it falls back to internal server-variable checks.
$this->app->session->set('user_id', 42);
$userId = $this->app->session->get('user_id');
if ($this->app->session->has('user_id')) {
// Logged-in state exists
}$this->app->session->start();$this->app->session->destroy();$this->app->session->regenerate(true);
$this->app->session->set('user_id', $userId);if ($this->app->session->isActive()) {
$sessionId = $this->app->session->id();
}The Session service does not provide:
- flash message handling
- CSRF protection
- remember-me authentication
- distributed session coordination
- custom session storage abstraction
- application-level authorization state
These concerns belong elsewhere.
In particular, flash handling belongs to the dedicated Flash service rather than the Session service.
Use the Session service when you need:
- authenticated user state
- short-lived server-side request continuity
- explicit logout and session teardown
- controlled session cookie behavior
- predictable session lifecycle management
Call start() explicitly when header timing matters.
Call regenerate() explicitly after login and privilege changes.
Use conservative fingerprint settings unless you fully control the client/network environment.
The CitOmni Session Service is a thin, deterministic, and security-aware wrapper around PHP's native session system.
It provides:
- a small public API
- lazy startup
- hardened defaults
- deterministic cookie policy resolution
- optional rotation
- optional fingerprint binding
- explicit and predictable failure semantics
Its purpose is not to be a session framework.
Its purpose is to provide a stable operational contract for session handling inside CitOmni applications.