Session Limit Enforcement
Introduction
Session limits prevent uncontrolled sharing of personal credentials by capping the total number of active sessions that a user can hold simultaneously—across all devices and entry points. The policy applies to both web (cookie-based) and mobile (JWT) logins because they now coexist inside the unified auth_sessions table.
Related Reading: See User Session Architecture for a deep-dive into session types, lifecycles, and security features.
Configuring the Limit
| Scope | Setting | Notes |
|---|---|---|
| Tenant | users.auth.sessions_limit.enabled | Global ON/OFF toggle. Disabled for PUBLICALA by design. |
| Tenant | users.auth.sessions_limit.default | Fallback integer used when the user-level value is NULL. |
| User | users.sessions_limit column | Allows per-user overrides; NULL means "use tenant default". |
A NULL in both tenant and user settings is interpreted as unlimited.
Configuration Precedence
The system follows a clear hierarchy when determining the session limit for a user:
| Priority | Source | Field | Behavior |
|---|---|---|---|
| 1 | User-specific | users.sessions_limit | Always takes precedence when set |
| 2 | Tenant default | users.auth.sessions_limit.default | Used when user value is NULL |
Precedence Examples
| User Limit | Tenant Default | Applied Limit | Explanation |
|---|---|---|---|
0 | 500 | 0 | User-specific value enforced (no sessions allowed) |
10 | 500 | 10 | User-specific value enforced |
NULL | 500 | 500 | Falls back to tenant default |
NULL | NULL | Unlimited | No limits when both are NULL |
Implementation Details
The precedence logic is implemented in SessionLimiter.php:
$this->limit = $limit ?? intval(tenant('users.auth.sessions_limit.default'));
Enforcement Flow
| # | Web Login | Mobile Login (API v2 JWT) |
|---|---|---|
| 1 | User submits credentials | App exchanges email + token for JWT |
| 2 | Laravel fires AfterLogin event | Validation endpoint prepares JWT & would create session rows |
| 3 | EnforceSessionLimit listener instantiates SessionLimiter using the web session id (session()->getId()) | Same SessionLimiter check runs using the app_jti, before persisting the auth_sessions rows |
| 4 | SessionLimiter counts current rows in auth_sessions for the user plus the prospective new one | Same counting logic (mixed web + mobile) |
| 5a | Limit not reached → login continues; session row is later persisted by the session handler | Limit not reached → JWT is issued, session is persisted with app_jti |
| 5b | Limit reached → login is aborted. User is redirected to sessionsLimit.index where they can close older sessions | Limit reached → API returns HTTP 409 Session limit reached and no token is issued |
Note: If the current browser or mobile session identifier (cookie
idfor web,app_jtifor mobile) already exists in the database it is always accepted, even when the count equals the limit. This prevents locking users out of sessions that are already active on their devices.
Mixed Sessions
SessionLimiter treats web and mobile rows equally. A user with a limit of 2 could have:
- 2 web sessions (desktop + laptop)
- 1 web + 1 mobile
- 2 mobile sessions
Any third attempt, regardless of type, will be blocked.
publica.la Reader Exception
In publica.la Reader app the limit is always ignored. This ensures that readers using the multi-tenant app are never blocked due to limits set for individual tenants.
We must ensure that content belonging to tenants with custom branded apps is excluded from the multi-tenant publica.la Reader app so that users cannot bypass the session limiter by switching apps. This will be addressed in a future improvement.
Remediation Options
- Close other sessions (Web UI): The
sessionsLimit.indexpage lists existing sessions and lets the user terminate selected ones to free slots. - Auth Sessions API: Admins can call the
DELETE /integration-api/v1/auth-sessions/(id)orDELETE /integration-api/v1/auth-sessions/users/(user_id)endpoints to force removal. - Mobile Logout: When a mobile app calls the logout endpoint, all rows with its
app_jtiare deleted across tenants, freeing slots immediately.
After remediation, the user can retry the login flow and it will succeed as long as the count is now ≤ limit.