diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php b/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php index 89afe9c367e..e9a905cbbbb 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php @@ -397,6 +397,7 @@ public function beforeExecuteRoute(Dispatcher $dispatcher) $this->view->session_username = !empty($_SESSION['Username']) ? $_SESSION['Username'] : '(unknown)'; $this->view->system_hostname = $cnf->object()->system->hostname; $this->view->system_domain = $cnf->object()->system->domain; + $this->view->session_timeout = $this->session_timeout; if (isset($this->view->menuBreadcrumbs[0]['name'])) { $output = []; diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerRoot.php b/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerRoot.php index 9d4c0e91c5a..cfdc63a59fb 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerRoot.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerRoot.php @@ -60,6 +60,11 @@ class ControllerRoot extends Controller */ protected $langcode = 'en_US'; + /** + * @var int session timeout in seconds + */ + public $session_timeout = 14400; + /** * set system language according to configuration */ @@ -130,9 +135,9 @@ public function doAuth() { $cnf = Config::getInstance()->object(); if (!empty($cnf->system->webgui->session_timeout)) { - $session_timeout = $cnf->system->webgui->session_timeout * 60; + $this->session_timeout = $cnf->system->webgui->session_timeout * 60; } else { - $session_timeout = 14400; + $this->session_timeout = 14400; } $redirect_uri = "/?url=" . $_SERVER['REQUEST_URI']; if ($this->session->has("Username") == false) { @@ -147,7 +152,7 @@ public function doAuth() return false; } elseif ( $this->session->has("last_access") - && $this->session->get("last_access") < (time() - $session_timeout) + && $this->session->get("last_access") < (time() - $this->session_timeout) ) { // session expired / cleanup session data $this->getLogger('audit')->notice(sprintf( @@ -164,7 +169,12 @@ public function doAuth() $this->setLang(); - $this->session->set("last_access", time()); + $is_ajax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; + $is_post = $_SERVER['REQUEST_METHOD'] === 'POST'; + + if (!$is_ajax || $is_post) { + $this->session->set("last_access", time()); + } // Authorization using legacy acl structure $acl = new ACL(); diff --git a/src/opnsense/mvc/app/views/layouts/default.volt b/src/opnsense/mvc/app/views/layouts/default.volt index 1a20186c331..277d159062b 100644 --- a/src/opnsense/mvc/app/views/layouts/default.volt +++ b/src/opnsense/mvc/app/views/layouts/default.volt @@ -42,6 +42,7 @@ diff --git a/src/opnsense/www/js/opnsense.js b/src/opnsense/www/js/opnsense.js index 310a5aa1b94..92933951222 100644 --- a/src/opnsense/www/js/opnsense.js +++ b/src/opnsense/www/js/opnsense.js @@ -370,3 +370,77 @@ function download_content(payload, filename, file_type) { } }); } + +const ACTIVITY_KEY = 'opn_last_activity'; +let sessionThrottleTimer = null; + +let lastPingTime = Date.now(); + +/** + * Resets the shared local storage timestamp. + */ +function resetSessionTimeout() { + if (!sessionThrottleTimer) { + sessionThrottleTimer = setTimeout(function() { + let now = Date.now(); + localStorage.setItem(ACTIVITY_KEY, now.toString()); + sessionThrottleTimer = null; + + if ((now - lastPingTime) > 300000) { + lastPingTime = now; + $.post('/api/core/menu/search'); + } + }, 1000); + } +} + +/** + * Initializes the auto-logout tracking mechanism. + */ +function initSessionTimeout() { + if (typeof window.sessionTimeout !== 'number' || window.sessionTimeout <= 0) { + return; + } + + const sessionTimeoutMs = window.sessionTimeout * 1000; + + if (!localStorage.getItem(ACTIVITY_KEY)) { + localStorage.setItem(ACTIVITY_KEY, Date.now().toString()); + } + + $(document).on('mousemove keydown click scroll touchstart', function(e) { + if (e.originalEvent === undefined || e.originalEvent.isTrusted === false) { + return; + } + resetSessionTimeout(); + }); + + // Explicit logout synchronization + $(document).on('click', 'a[href*="logout"]', function() { + localStorage.setItem('opnsense_logout', Date.now().toString()); + localStorage.removeItem(ACTIVITY_KEY); + }); + + window.addEventListener('storage', function(e) { + if (e.key === 'opnsense_logout' || (e.key === ACTIVITY_KEY && !e.newValue)) { + window.location.reload(); + } + }); + + setInterval(function() { + let activeKeyStr = localStorage.getItem(ACTIVITY_KEY); + + if (!activeKeyStr) { + window.location.reload(); + return; + } + + let lastActive = parseInt(activeKeyStr, 10); + let timeIdleMs = Date.now() - lastActive; + + if (timeIdleMs > (sessionTimeoutMs + 10000)) { + localStorage.removeItem(ACTIVITY_KEY); + window.location.reload(); + } + }, 5000); +} diff --git a/src/www/authgui.inc b/src/www/authgui.inc index 67cf31d9b11..c28ef808c28 100644 --- a/src/www/authgui.inc +++ b/src/www/authgui.inc @@ -380,7 +380,19 @@ function display_login_form($Login_Error) - + diff --git a/src/www/head.inc b/src/www/head.inc index 9a345494124..eed55353d8b 100644 --- a/src/www/head.inc +++ b/src/www/head.inc @@ -112,6 +112,8 @@ $pagetitle .= html_safe(sprintf(' | %s.%s', $config['system']['hostname'], $conf +