Specs v2

Tags: #security #sessions
GLPI version: 12.0+

Overview

GLPI has no way to see active sessions or terminate them remotely. If an account is compromised or a device is lost, there is no targeted response.

This feature adds persistent tracking of web sessions and OAuth tokens, an admin interface with audit trail, and a user self-service panel.


Scope

In — web sessions (PHP file-based), OAuth 2.0 access/refresh tokens, connection history, admin UI, user self-service, REST API, CLI, Setup → Security menu restructure.

Out — OAuth client management (already in Administration → OAuth Clients), legacy personal API tokens (personal_token in glpi_users), suspicious session detection (IP/User-Agent change mid-session — deferred to a later phase).


Personas & User Stories

Super-Admin — all sessions, all entities

ID

Story

Priority

SA-01

View all active sessions (browser + API) in a single list with type badges

Must

SA-02

See per session: user, type, IP or client+scopes, device, last activity, auth method

Must

SA-03

Filter by user, IP, entity, type, auth type, date range

Must

SA-04

Revoke any specific session or token

Must

SA-05

Revoke all sessions for a user

Must

SA-06

Revoke all sessions globally (excluding own)

Must

SA-07

From a user's form, view that user's active sessions and history (same page as U-01–03)

Should

SA-08

Every revocation recorded in the audit log

Must

Admin — same as Super-Admin, scoped to accessible entity tree

Regular User — own sessions only

ID

Story

Priority

U-01

View all active sessions and API tokens; current session clearly identified

Must

U-02

Revoke any session or token (not current browser session)

Must

U-03

View my connection history with end reason

Should

SA-07 and U-01–03 are served by the same Sessions tab on the User form — visible to the user themselves and to admins. Revoke buttons respect the same access rules in both contexts.

Why allow user self-service? Industry standard (GitHub, Google); GDPR Article 15 right of access; reduces admin load; users often detect unauthorised access first.

CLI

ID

Story

Priority

C-01

List active sessions

Must

C-02

Revoke sessions for a user or globally

Must


Security Menu Restructure

Password policy and 2FA currently live inside Setup → General → Security tab. They move to a new first-level item:

Setup
├── General          (Security tab removed)
├── Security         ← new
│   ├── Password Policy    (moved)
│   ├── Two-Factor Auth    (moved)
│   └── Sessions           (new)
└── …

No functional change to Password Policy or 2FA — settings-only reorganisation. Requires a new SecurityConfig class implementing getMenuContent(), extracted template partials, and updated breadcrumbs.


Data Model

glpi_user_sessions — active sessions only

Rows are deleted (not flagged) when a session ends for any reason.

Column

Type

Notes

id

int PK


users_id

int FK glpi_users


session_token_hash

varchar(64) UNIQUE

hash('sha256', session_id())

session_file

varchar(261) NULL

sess_<id> filename, stored for unlink() on revocation

ip_address

varchar(45)

IPv4 or IPv6

user_agent

text

Raw — parsed lazily at display time

auth_type

tinyint

Auth class constants

created_at

timestamp


last_activity_at

timestamp

Updated on every request

Indexes: session_token_hash (unique), users_id, last_activity_at.



Why store session_file? The raw session ID cannot be reconstructed from its hash, so we store the filename directly to unlink() on revocation. If deletion fails, cronSession() cleans it up on the next run — an acceptable short-lived gap. Both files and redis backends are supported; see the Redis Session Backend section. v2 path: switch to a DB-backed \SessionHandlerInterface and drop this column (~1–2 weeks; main challenge is session locking for concurrent AJAX via SELECT … FOR UPDATE).


glpi_user_session_history — immutable audit log

Inserted at login. Rows are kept indefinitely.

Column

Type

Notes

id

int PK


users_id

int FK


session_token_hash

varchar(64)

Correlation key — indexed, not a FK

ip_address

varchar(45)


user_agent

varchar(512)

Raw, truncate before inserting

auth_type

tinyint


logged_in_at

timestamp


logged_out_at

timestamp NULL


logout_reason

enum NULL

user / admin / expired / forced

revoked_by_users_id

int NULL FK glpi_users

Set when logout_reason = 'admin'

At session end: identify the history row by session_token_hash, then write logged_out_at, logout_reason, and users_id_revoked_bybefore deleting the active row.

Indexes: session_token_hash, (users_id, logged_in_at DESC).


Business Logic

Session lifecycle

-- Login (Session::init)
  → INSERT glpi_user_sessions
  → INSERT glpi_user_session_history  (logged_out_at = NULL)

-- Every authenticated request
  → UPDATE glpi_user_sessions SET last_activity_at = NOW() WHERE session_token_hash = ?

-- Normal logout (Session::destroy)
  → DELETE FROM glpi_user_sessions WHERE session_token_hash = ?
  → UPDATE glpi_user_session_history SET logged_out_at = NOW(), logout_reason = 'user'
       WHERE session_token_hash = ?

-- Admin / user revoke (SessionTracker::revokeSession)
--  → Assert target ≠ caller's current browser session
--  → Browser: destroySessionStorage($session_file)  (see Redis section)
    DELETE FROM glpi_user_sessions WHERE session_token_hash = ?
    UPDATE glpi_user_session_history SET logged_out_at = NOW(),
    logout_reason = 'admin', users_id_revoked_bybefore  = ?
    WHERE session_token_hash = ?
-- cookie_token is a single token shared across all devices for a given user.
-- Admin revocation bypasses the normal logout flow, so without an explicit clear
-- the target user would be silently re-authenticated via cookie on the next request.
    UPDATE glpi_users SET cookie_token = NULL, cookie_token_date = NULL
    WHERE id = <target users_id>
--  → API:     AccessTokenRepository::revokeAccessToken() + cascade refresh tokens
--  → Write audit log entry (target user, acting user, session details, timestamp)


Per-request update

On every authenticated request, a single fire-and-forget UPDATE refreshes last_activity_at:

UPDATE glpi_user_sessions
SET last_activity_at = NOW()
WHERE session_token_hash = ?


User-Agent — lazy resolution

Raw User-Agent strings are stored at login. Browser name and OS are parsed with matomo/device-detector at render time only — never in the request path.


Cron — extend CronTask::cronSession()

The existing Clean expired sessions action works purely at the filesystem level (glob + delete by age). The file deletion step is kept unchanged. A separate DB pass is added after it; Find and delete glpi_user_sessions rows where last_activity_at < NOW() - INTERVAL <gc_maxlifetime> SECOND; update corresponding history rows with logout_reason = 'expired'. This covers sessions that were missed by the file cleanup (e.g. due to session.save_handler misconfiguration) or where the file was deleted but the DB row was not.


UI

Unified session list

Active sessions and connection history share a single table used in both the admin view and the user tab. Rows from glpi_user_sessions (active) and glpi_user_session_history (ended) are merged and sorted by login time descending. A Status column differentiates them.

Column

Content

Type

Browser or API badge

User (admin only)

Linked username

Details

Browser + OS (parsed at render time) for browser sessions; client name · scopes for API tokens

IP Address

For browser sessions only; — for API tokens

Login

Relative date (with absolute on hover) for less than 24h sessions

Last activity

same

Status

Active badge for active sessions (API tokens also show the expiry date); logout reason badge (User logout / Admin revoke / Expired) for ended sessions

Actions

Revoke button for active sessions; empty for ended sessions

The revoke button reads data-type from the row and calls the appropriate endpoint — both use the same glpi_confirm() → AJAX → remove row → toast flow.

Administration → Sessions

Access: Super-Admins only (Config:UPDATE right), scoped to their entity tree.

Single page with the unified list and a classic GLPI Search form.
Default criteria: Status = Active only. Switching to All reveals ended sessions from glpi_user_session_history interleaved by login date.

Criteria:

  • User search,

  • Status (Active only / All), by detault Active only

  • Type (Browser / API)

  • IP address

  • Date range.

Small mockup to illustrate the concept — final design can differ.



User form → Sessions tab

Added to src/User.php. Visible in two contexts:

  • Administration → Users → [user] — admin views sessions and history for that user (SA-07)

  • My Settings — user views their own sessions (U-01–03)

Single unified list filtered to this user; no User column. Active and ended sessions are shown together with no filter — the user sees their full history at a glance. Current browser session row shows a "This device" badge with no Revoke button.

Header action: Sign out all sessions (admin) or Sign out all other sessions (self) — operates on active rows only.

Small mockup to illustrate the concept — final design can differ.


REST API

Bearer token auth on /api.php/vx.y/.

These endpoints are proposed — discuss before implementing whether REST exposure is needed in v1. This is not mandatory for the current scope of work, but it could be needed for usage of alternative clients (e.g. mobile) or third-party integrations. The API should be designed to mirror the UI capabilities as closely as possible, with the same access control rules.

Method

Path

Description

Rights

GET

/SessionManager/

List active browser sessions

Admin

DELETE

/SessionManager/{id}

Revoke browser session

Admin or own (not current)

DELETE

/SessionManager/user/{users_id}

Revoke all sessions for user

Admin

GET

/User/{id}/Sessions

Active sessions + tokens

Self or Admin

GET

/User/{id}/SessionHistory

Connection history

Self or Admin


CLI

Command

Options

Description

glpi:session:list

--user=<name|id>

List active sessions

glpi:session:revoke

--session=<id> | --user=<name|id> | --all, --force

Revoke sessions

--force skips confirmation. --all excludes the caller's current session. Exit codes: 0 success · 1 error · 2 aborted.


Redis Session Backend

Both files and redis session backends are supported. The revocation mechanism is encapsulated in SessionTracker::destroySessionStorage(string $session_file): void so the rest of the flow stays unchanged.

Key derivation

The session_file column stores the PHP session filename (sess_<session_id>). The raw session ID is extracted by stripping the sess_ prefix: $session_id = substr($session_file, 5). This is the key used directly in Redis — no prefix.

Implementation

private static function destroySessionStorage(string $session_file): void
{
    $session_id = substr($session_file, 5); // strip 'sess_'

    if (ini_get('session.save_handler') === 'redis') {
        $redis = new \Redis(/* connection parsed from ini_get('session.save_path') */);
        $redis->del($session_id);
        return;
    }

    @unlink(GLPI_SESSION_DIR . '/' . $session_file);
}

Cron task

The existing filesystem glob (glob(GLPI_SESSION_DIR . '/sess_*')) has no effect on Redis. PHP's Redis session handler sets an EXPIRE TTL on each key automatically (based on session.gc_maxlifetime), so storage-level expiration is handled natively. The DB reconciliation pass in cronSession() runs identically for both backends.

System requirements check

A non-blocking warning is shown in Administration → System when session.save_handler is set to anything other than files or redis, as those are the only two supported backends:

Session save handler is set to <value>. Only files and redis are supported. Session revocation may not work correctly.


Security

Token storage — session_token_hash = hash('sha256', session_id()). session_file stores only the filename; DB compromise already implies full access, so the incremental risk is marginal.

Access control — users read/revoke only their own sessions; nobody can revoke their own current browser session;

Remember-me on revocation — glpi_users.cookie_token is a single token per user (not per device). Clearing it on revocation invalidates remember-me on all the user's devices simultaneously — an acceptable trade-off given the current architecture. See the Session lifecycle for the exact steps at login, logout, and revocation.

CSRF — UI revoke actions use GLPI's existing CSRF token. API is stateless.

Audit trail — every revocation writes a GLPI Log entry: target user, acting user, session details, timestamp.

Data retention — IP addresses are personal data. History is kept indefinitely; administrators should review this against applicable privacy regulations and purge manually if required.

Non-file session backends — See the dedicated section below.