04.08 Session Management and Timeout
SimpleRisk sessions have two timeout layers — an idle timeout (default 1 hour) and an absolute lifetime (default 8 hours). Both are configurable. Sessions are stored via a custom database-backed handler, garbage-collected on cron, and can be force-terminated per user when permissions or credentials change.
Why this matters
Sessions are the trust window between a user authenticating and the application accepting their identity. Tune them wrong and you either frustrate users (re-login every 15 minutes) or you create exploitable durability (a session that lasts forever survives the user closing their laptop in a coffee shop). Most security frameworks have an opinion on session lifetime; most users have an opinion on how often they want to re-authenticate. Picking the right values for your environment is a small configuration step with outsized impact on both posture and friction.
The honest scope to know up front: SimpleRisk uses two timeout layers, not one. There's an idle timeout (kill the session if the user does nothing for X seconds) and an absolute timeout (kill the session no later than X seconds after creation, regardless of activity). Default: 3600 seconds (1 hour) idle and 28800 seconds (8 hours) absolute. Both can be changed independently. Most operators only know about one of the two and are surprised when the other one fires.
The other thing worth knowing: the session cache holds the user's permission set. As covered in The Permission Model, set_user_permissions() writes the user's effective permissions into $_SESSION at login. Permission changes don't take effect until the session ends and a new one starts. The session timeout is therefore also the upper bound on how long stale permissions persist after a change. For programs with strict deprovisioning requirements, this matters: a session lasting 8 hours means a revoked user can keep using the application for up to 8 hours after the revocation.
The third thing: sessions are stored in the database, not in PHP's default file-system handler. SimpleRisk implements a custom SimpleRiskSessionHandler that delegates to sess_open(), sess_close(), sess_read(), sess_write(), sess_destroy(), sess_gc() functions backed by a sessions table. This makes sessions visible across multiple application servers (fronted by a load balancer) and queryable from the database. It also means session storage is your database storage; high-volume installs should monitor session table growth.
Before you start
Have these in hand:
- Admin access to Configure → Settings for the timeout configuration.
- A clear understanding of the program's session-lifetime requirements. Common reference points: NIST SP 800-63B suggests AAL2 sessions can last up to 12 hours of activity (with 30-minute idle); PCI DSS requires 15-minute idle timeout for systems handling cardholder data; SOC 2 type 2 controls typically expect documented and enforced session lifetimes.
- Awareness that changes affect existing sessions. Tightening the timeout doesn't kill existing sessions; they live their natural life, and the new timeout applies only to new sessions. Loosening the timeout doesn't extend existing sessions either; the existing session's original timeout window still controls. The change is forward-looking.
Step-by-step
1. Decide on your timeout values
Two values to set. Match to your program's requirements:
Low-friction internal app
- Idle timeout: 8 hours (28800s)
- Absolute lifetime: 24 hours (86400s)
- Notes: Users authenticate once a workday; tradeoff is durability against potential session theft.
Standard SaaS posture
- Idle timeout: 1 hour (3600s) — the SimpleRisk default
- Absolute lifetime: 8 hours (28800s) — the SimpleRisk default
- Notes: Default. Workday-friendly; users re-auth on lunch returns and at end of day.
Compliance-driven (PCI, SOC 2)
- Idle timeout: 15 minutes (900s)
- Absolute lifetime: 4 hours (14400s)
- Notes: Frequent re-auth; appropriate for systems holding sensitive data.
High-security / classified
- Idle timeout: 5 minutes (300s)
- Absolute lifetime: 1 hour (3600s)
- Notes: Aggressive; user friction is significant; appropriate only when warranted.
Pick the row that matches your program's posture. Defaulting to the SimpleRisk defaults is a defensible choice if no specific framework drives a tighter value.
2. Configure the idle timeout
Sidebar: Configure → Settings. Find the Session Activity Timeout (or equivalent label; session_activity_timeout in the settings table). Value is in seconds.
Save. The setting takes effect for new sessions immediately; existing sessions continue under their original timeout.
3. Configure the absolute lifetime
Same settings page. Find the Session Absolute Timeout (or equivalent label; session_absolute_timeout). Value is in seconds.
Save. Same forward-looking behavior as the idle timeout.
The absolute lifetime should always be greater than the idle timeout (otherwise the absolute kills sessions before idle has a chance to). The default ratio (8 hours absolute, 1 hour idle) is a reasonable starting point — the user can be active continuously for 8 hours, but a 1-hour idle still kills sessions where the user walked away.
4. Configure session GC frequency
The session garbage collector (sess_gc()) runs periodically to remove expired session rows from the sessions table. The default GC interval (1440 minutes / 24 hours) is set in PHP's session configuration; it's appropriate for most installs.
The GC doesn't affect when sessions expire (that's the timeout settings); it affects when expired-session rows are physically deleted from the database. For high-volume installs where the sessions table grows large between GCs, a more frequent GC reduces table size at the cost of more DB churn. Most installs leave it alone.
5. Verify the timeouts with a test account
Don't trust the configuration; verify it.
Idle timeout test:
- Log in as a non-admin test user.
- Note the time.
- Walk away (or open a stopwatch tab) for slightly longer than the idle timeout.
- Click anything in the application.
- Confirm you're redirected to the login page.
Absolute timeout test:
- Log in as a non-admin test user.
- Note the time.
- Stay actively clicking (or set up a tab that periodically polls) so the idle timeout doesn't fire.
- Continue clicking until past the absolute timeout.
- Confirm you're redirected to the login page.
If either test doesn't behave as expected, check that the setting actually saved (SELECT name, value FROM settings WHERE name LIKE 'session_%';) and that the value is in seconds (not minutes — the most common misread).
6. Force-terminate sessions for a user when needed
A few scenarios require killing a user's active sessions immediately rather than waiting for timeout:
- Permission revocation that needs immediate effect (a user is reclassified or removed from a sensitive team).
- Suspected credential compromise (a user's password or API key may have leaked).
- Departing user (employee offboarding).
The function kill_sessions_of_user($user_id) (in simplerisk/includes/authenticate.php) clears all active sessions for a specific user. It's called automatically on:
- Password change (the user's new credential should be the only valid one going forward).
- MFA enable / disable / reset.
- The user being explicitly disabled via User Management.
For ad-hoc force-logout, the operator path:
- Open Configure → User Management → [user].
- Disable the user (which triggers
kill_sessions_of_user()). - If the user shouldn't be permanently disabled, re-enable after a delay sufficient to ensure sessions are gone.
For mass force-logout (every user logged out simultaneously, e.g., during a maintenance window): the path is to truncate the sessions table from the database (DELETE FROM sessions;). This is destructive and forces every active user to re-authenticate; do it during a planned window, not casually.
7. Plan for high-availability (multi-server) deployments
If you run multiple SimpleRisk application servers behind a load balancer, the database-backed session storage is what makes that work — both servers see the same sessions table. A few considerations:
- Session affinity (sticky sessions) is not required because both servers can read any session. But sticky sessions can still help by keeping a user's requests on the server that already has their session in memory cache.
- Database performance is the bottleneck for session reads. The
sessionstable is queried on every authenticated request; it should be on fast storage with appropriate indexes (the schema's PK onidis sufficient for typical loads). - Clock skew between servers matters less than for file-based sessions because the timeout calculation uses the database's view of "now" (or the application's view of "now," which is close enough as long as servers are NTP-synced).
8. Plan for session-related telemetry
The sessions table is also a useful source for operational telemetry:
- Active session count:
SELECT COUNT(*) FROM sessions WHERE access > UNIX_TIMESTAMP() -gives you currently-active sessions.; - Sessions per user:
SELECT user_id, COUNT(*) FROM sessions GROUP BY user_id;shows users with multiple concurrent sessions (one user logged in from two browsers, or potentially session theft if the user only owns one device). - Stale-session rate: comparing total
sessionsrows vs active rows shows how much of the table is awaiting GC.
For programs running monitoring dashboards, surfacing these as metrics gives you early warning of unusual login patterns.
Common pitfalls
A handful of patterns recur with session management.
-
Setting the idle timeout in minutes thinking it's seconds (or vice versa). The settings are in seconds.
1for "1 hour" is actually 1 second;60is 60 seconds (1 minute). Verify by computing: 1 hour = 3600 seconds, 8 hours = 28800 seconds, 15 minutes = 900 seconds. -
Setting the absolute timeout below the idle timeout. The absolute kills sessions before idle gets a chance, making the idle setting meaningless. Always: absolute > idle.
-
Tightening session timeouts without communicating the change. Users mid-task get logged out and lose unsaved work. Pre-announce changes; consider a maintenance-window cutover.
-
Expecting permission revocations to take effect immediately. Permissions are session-cached. Either accept the up-to-session-lifetime delay or use
kill_sessions_of_user()(via disabling the user) to force immediate effect. -
Keeping a single ultra-long session for "convenience." A 30-day session that survives every reboot is wonderful for the user and terrible for security. The time between a credential being stolen and the session expiring is the attacker's window of opportunity. Match the session lifetime to the program's risk tolerance.
-
Treating the GC as the timeout enforcement. The GC removes expired rows; the timeout enforcement happens at request time (
session_check()). A very long GC interval doesn't mean sessions live longer; it just means the database holds more dead rows until cleanup. -
Not monitoring
sessionstable size on high-volume installs. A 10,000-user install with hourly login churn produces tens of thousands of session rows per day. With a 24-hour GC interval, the table holds the last day's worth even after sessions expire. Increase GC frequency or accept the table size; either is fine, but know which. -
Forgetting that the session store is the database. A database outage means sessions can't be read; users get logged out (or auth fails). Database HA is therefore also session HA. Plan accordingly for production.
-
Leaving sticky sessions on as a load-balancing default. Sticky sessions aren't required for SimpleRisk's database-backed sessions, but they're often configured by default at the load-balancer layer. Sticky-session behavior can mask issues (a session that only works against one app server because it's secretly file-based and the LB is hiding the failure case). Verify the database session storage is actually being used.
-
Killing all sessions during business hours.
DELETE FROM sessions;works but logs out every user, mid-task. Save it for maintenance windows; for individual users, use the User Management disable / enable cycle. -
Not factoring API key sessions into the timeout calculation. API requests don't create persistent sessions in the same way browser logins do — each API request is auth-then-respond, with no session row created. The session-timeout configuration doesn't apply to API access; key revocation is the equivalent (see API Key Management).
Related
- Local Authentication and Password Policies
- Multi-Factor Authentication
- The Authentication Extra Overview
- API Key Management
- The Permission Model
- Managing Users, Teams, and Roles
Reference
- Permission required:
check_adminfor the session-timeout settings;check_adminfor the User Management disable/enable cycle that triggers per-user session termination. - API endpoint(s): None specific to session configuration.
- Implementing files:
simplerisk/includes/authenticate.php(session_check(),kill_sessions_of_user(),deauthenticate(), theSimpleRiskSessionHandlerclass withsess_open(),sess_close(),sess_read(),sess_write(),sess_destroy(),sess_gc()). - Database tables:
sessions(the database-backed session store);user(change_password,lockoutcolumns can effectively invalidate sessions on next request). config_settingskeys:session_activity_timeout(idle timeout, seconds — default3600);session_absolute_timeout(absolute lifetime, seconds — default28800).- PHP session settings:
session_set_save_handler()registers the customSimpleRiskSessionHandler; GC probability and interval set via standard PHPsession.gc_*ini directives. - External dependencies: None.