Skip to main content

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

ScopeSettingNotes
Tenantusers.auth.sessions_limit.enabledGlobal ON/OFF toggle. Disabled for PUBLICALA by design.
Tenantusers.auth.sessions_limit.defaultFallback integer used when the user-level value is NULL.
Userusers.sessions_limit columnAllows 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:

PrioritySourceFieldBehavior
1User-specificusers.sessions_limitAlways takes precedence when set
2Tenant defaultusers.auth.sessions_limit.defaultUsed when user value is NULL

Precedence Examples

User LimitTenant DefaultApplied LimitExplanation
05000User-specific value enforced (no sessions allowed)
1050010User-specific value enforced
NULL500500Falls back to tenant default
NULLNULLUnlimitedNo 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 LoginMobile Login (API v2 JWT)
1User submits credentialsApp exchanges email + token for JWT
2Laravel fires AfterLogin eventValidation endpoint prepares JWT & would create session rows
3EnforceSessionLimit listener instantiates SessionLimiter using the web session id (session()->getId())Same SessionLimiter check runs using the app_jti, before persisting the auth_sessions rows
4SessionLimiter counts current rows in auth_sessions for the user plus the prospective new oneSame counting logic (mixed web + mobile)
5aLimit not reached → login continues; session row is later persisted by the session handlerLimit not reached → JWT is issued, session is persisted with app_jti
5bLimit reached → login is aborted. User is redirected to sessionsLimit.index where they can close older sessionsLimit reached → API returns HTTP 409 Session limit reached and no token is issued

Note: If the current browser or mobile session identifier (cookie id for web, app_jti for 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.

TODO

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

  1. Close other sessions (Web UI): The sessionsLimit.index page lists existing sessions and lets the user terminate selected ones to free slots.
  2. Auth Sessions API: Admins can call the DELETE /integration-api/v1/auth-sessions/(id) or DELETE /integration-api/v1/auth-sessions/users/(user_id) endpoints to force removal.
  3. Mobile Logout: When a mobile app calls the logout endpoint, all rows with its app_jti are 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.

X

Graph View