User Session Architecture with Refresh Tokens
Introduction
This document describes how the platform manages web, guest, and mobile app (JWT) sessions in a unified way using the auth_sessions table. The design provides strong security, clear observability, and user/admin control.
Session Types
| Type | Identifier in auth_sessions | Creation Moment | Revival | Notes |
|---|---|---|---|---|
| Web | app_jti = NULL, may have refresh_token_hash | At web login or from integrations | Yes (refresh-token) | 2 h inactivity timeout |
| Guest | user_id = NULL, app_jti = NULL | Automatic when browsing | No | Deleted on inactivity |
| Mobile | app_jti ≠ NULL (JWT jti) | At JWT issuance (rows pre-created for all current tenants) | No | Expires after 1 year |
Lifecycle
Creation
- Web – created on successful web login. If "remember" is used during the login, a refresh token is generated for the session
- Mobile – when the API issues a JWT, the backend:
- Extracts the token's
jti. - Enumerates every tenant the user currently belongs to.
- Inserts one session row per tenant with
app_jti = jti. - Each row holds
last_activity = now(). - Each row is expirable individually by the user/admins
- Extracts the token's
- Guest – created automatically for anonymous browsing.
Expiration
- Guest sessions and web sessions without refresh token expire after 120 min of inactivity (
last_activitycomparison). - Web sessions with refresh token become inactive after 120 min but can be revived (see Revival section).
- Mobile sessions expire after 1 year from creation (JWT expiration time).
Revival
Only web sessions that still hold a valid refresh-token can be revived (handled by HandleSessionRefreshToken).
Mobile sessions do not support revival; users should simply obtain a new JWT after a 401 status.
Termination
Sessions can be terminated through several mechanisms:
User-Initiated Termination
- Direct logout: When users log out, their current session is deleted
- My Account management: Users can close other active sessions from the "My Account" section
Administrative Termination
- Auth Sessions API: Admins can terminate user sessions via the auth sessions API endpoints
Mobile App Logout
- Cross-app logout: When a user logs out from any mobile app (Publica.la Reader or custom tenant apps), all sessions containing that specific
app_jtiare deleted across all tenants - Immediate effect: After deletion, any request using that JWT receives HTTP 401 status
Session Cleanup (auth:purge-sessions)
Runs every hour at minute 25.
- Web sessions without a refresh token are deleted immediately after they become inactive, as they cannot be revived.
- Web sessions with a refresh token are purged only if their
last_activityis older than the purge threshold (30 days). - Mobile sessions: Currently excluded from the cleanup process initially. They persist until manually deleted by users, admins, or app logout.
The logic is decoupled from refresh token expiration to allow future extensibility (e.g., tracking dormant sessions separately).
A --dry-run option is available to simulate purges without deleting any data.
Refresh Token (Web Sessions Only)
Stored securely in the database (refresh_token_hash) and client cookie (session_refresh_token).
Rotation cadence and race-condition mitigation
The refresh token rotates intelligently to avoid race conditions and limit replay opportunities:
CustomDatabaseSessionHandlerissues a new refresh token only if: • the_issue_refresh_tokensession flag is present and
• the timestamp_last_refresh_rotated_atshows that at least 10 minutes have elapsed since the previous rotation.- When a rotation occurs the handler updates
_last_refresh_rotated_at. Concurrent requests read the fresh timestamp and skip their own rotation, preventing double issuance.
This strategy keeps the replay window under 10 minutes, eliminates the race condition, and avoids extra database queries.
Tokens still expire 30 days after their most recent issuance.
Only web sessions with a non-expired refresh token can be revived.
Data Model
Each session is stored in the auth_sessions table with the following key fields:
id: Session ID (primary key, matches cookie for web sessions)user_id: Linked user (nullable for guest sessions)last_activity: UNIX timestamp of last activity (Laravel sessions use UNIX instead of datetime)is_active: Indicates if the session is still validrefresh_token_hash: Hashed version of the refresh token (nullable, only for web sessions)refresh_token_expires_at: Token expiration timestamp (only for web sessions)app_jti: JWT identifier for mobile sessions (null for web sessions)user_agent,ip_address,auth_entry_point, etc. for auditingpayload: JSON field storing mobile app information (app_name, app_version, os, os_version)
Security Considerations
Refresh Token Rotation: Refresh tokens are rotated on each request to mitigate replay attacks. When an attacker intercepts a refresh token (through network sniffing or XSS), the token becomes useless after the legitimate user makes their next request, as the system generates a new token and invalidates the old one.
Session ID Regeneration: Session IDs are regenerated on login and refresh to prevent session fixation attacks. In this attack, an attacker creates a session ID and tricks a user into authenticating with that known ID (through phishing or URL manipulation). By regenerating the session ID after authentication, we ensure the attacker cannot access the authenticated session even if they know the original ID.
Expired Session Invalidation: Sessions that are expired and not revivable are treated as completely invalid. This prevents session resurrection attacks where an attacker might try to reuse old session data from abandoned or compromised devices. Once a session expires without a valid refresh token, it cannot be restored under any circumstances.
JWT Security: For mobile sessions, JWTs maintain statelessness while server-side tracking provides observability. The app_jti field allows for explicit token revocation when needed.
Domain Placement
Session logic resides in the Identity domain.
CustomDatabaseSessionHandler: Reads, writes, and structures session data.
HandleSessionRefreshToken middleware: Handles revival logic and re-authentication for web sessions.
SingleTenantAuthMiddleware: Handles mobile session tracking during JWT validation. It will also handle creation during the initial grace period
AuthSession model: Encapsulates session behavior and query scopes for both web and mobile sessions.
Session Visibility for Users
My Account Session Management
Users can view and manage their active sessions through the My Account section, which provides a unified interface for all session types:
Session Display
- Web sessions: Displayed with browser and device information derived from user agent strings
- Mobile sessions: Shown with "Mobile App" indicators, identified by the presence of
app_jtifield - Session details: Each entry includes last activity time, device/platform information, and IP address when available
User Actions
- View all active sessions: See sessions across web browsers and mobile applications
- Terminate individual sessions: Users can close specific sessions (e.g., "log out of other devices")
- Session information: Access details about when and where each session was created
Visibility Rules
- User-initiated sessions only: Only web and mobile sessions created by the user are displayed
- Impersonated sessions excluded: Administrative impersonation sessions are hidden from end users
This unified approach allows users to maintain control over their account security across all platforms and devices.
Session Data Exposure
API Response Structure
The AuthSessionResource exposes session information through two fields for backward compatibility and gradual migration:
user_agent_info(deprecated): Maintained for existing API consumers. Not displayed in the public doc.session_data(recommended): New field with identical structure for web, and a new one for mobile.
Both fields are populated from the sessionData attribute in the AuthSession model.
Session Data Attributes
The AuthSession model provides a unified sessionData attribute that returns different structures based on session type:
Web Sessions (app_jti = null)
[
'browser' => 'Chrome', // Parsed from user_agent
'device' => 'Desktop', // Detected device type
'os' => 'macOS', // Detected operating system
'type' => 'web', // Session type identifier
'raw' => 'Mozilla/5.0...' // Original user_agent string
]
Mobile Sessions (app_jti != null)
[
'app_name' => 'Publica Reader', // From payload JSON
'app_version' => '2.1.0', // From payload JSON
'os' => 'iOS', // From payload JSON
'os_version' => '17.2', // From payload JSON
'type' => 'mobile', // Session type identifier
'raw' => 'PublicaReader/2.1.0...' // Original user_agent string
]