diff --git a/config/config.sample.php b/config/config.sample.php index 00e3a6779fd477634bebcbaa607f1c7c5910bc2a..268838a1768cbdcf74f1b0152e7662d0995246b7 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -251,6 +251,15 @@ $CONFIG = [ */ 'session_keepalive' => true, +/** + * Enable or disable the automatic logout after session_lifetime, even if session + * keepalive is enabled. This will make sure that an inactive browser will be logged out + * even if requests to the server might extend the session lifetime. + * + * Defaults to ``false`` + */ +'auto_logout' => false, + /** * Enforce token authentication for clients, which blocks requests using the user * password for enhanced security. Users need to generate tokens in personal settings diff --git a/core/js/dist/install.js b/core/js/dist/install.js index 222e5218294432567e8f04dc14f18b3e4d908602..a40b65981c68204030be11119380cb1e3b446aca 100644 Binary files a/core/js/dist/install.js and b/core/js/dist/install.js differ diff --git a/core/js/dist/install.js.map b/core/js/dist/install.js.map index 062f9f7cd34a764002f899f12e77363dec29df1c..3652ba687c41a08a13b7ea0e9ac0e4185817976d 100644 Binary files a/core/js/dist/install.js.map and b/core/js/dist/install.js.map differ diff --git a/core/js/dist/login.js b/core/js/dist/login.js index 21aca3a274ae18526c4180c548cf0cac2491e56e..c5c8e8da9291d49ed7b56e401a6ba8e298d9f4e1 100644 Binary files a/core/js/dist/login.js and b/core/js/dist/login.js differ diff --git a/core/js/dist/login.js.map b/core/js/dist/login.js.map index 238250af27d1da29bf9bcc7cd8b75de8abb2ed85..76f2786eac9e939e63523e004f4f107cb00b8e8b 100644 Binary files a/core/js/dist/login.js.map and b/core/js/dist/login.js.map differ diff --git a/core/js/dist/main.js b/core/js/dist/main.js index c7f70650cfdbbc7a8846d6c8b87a7053d0267d0d..342e3c8730025486200085a7821141ada969bc0c 100644 Binary files a/core/js/dist/main.js and b/core/js/dist/main.js differ diff --git a/core/js/dist/main.js.map b/core/js/dist/main.js.map index 064e9125708317f6ae6a6f4f25f7e34152b9c83c..58d7bd722b013a3f8287a60cf5bed306cb312023 100644 Binary files a/core/js/dist/main.js.map and b/core/js/dist/main.js.map differ diff --git a/core/js/dist/maintenance.js b/core/js/dist/maintenance.js index f3ee56f65175825a20f29731b85fe0296b27f1f1..49a9c1a701f7c03a2180be2d9b03ae702bdc15a9 100644 Binary files a/core/js/dist/maintenance.js and b/core/js/dist/maintenance.js differ diff --git a/core/js/dist/maintenance.js.map b/core/js/dist/maintenance.js.map index 4b30e1dce7f8f6759559f712a2d2db8837e388c8..c91be959a72b67352595f872601d7cbe5ab91539 100644 Binary files a/core/js/dist/maintenance.js.map and b/core/js/dist/maintenance.js.map differ diff --git a/core/js/dist/recommendedapps.js b/core/js/dist/recommendedapps.js index a2f5dc8cab1c3d8a4add5d7c747ea9d09503c2d8..37086e049701b84179a9067abcb3ce1845d022f8 100644 Binary files a/core/js/dist/recommendedapps.js and b/core/js/dist/recommendedapps.js differ diff --git a/core/js/dist/recommendedapps.js.map b/core/js/dist/recommendedapps.js.map index 403c8fdf849fa814e288902c57e0f611b9a872fb..ab10a457b189f5c92b489631b7888807aa9f6986 100644 Binary files a/core/js/dist/recommendedapps.js.map and b/core/js/dist/recommendedapps.js.map differ diff --git a/core/src/session-heartbeat.js b/core/src/session-heartbeat.js index a941720d853b417cd0ecf5234c73ef485cc81d82..9c9148d2c7779f3aa8f958480123d024149b7cb0 100644 --- a/core/src/session-heartbeat.js +++ b/core/src/session-heartbeat.js @@ -21,18 +21,34 @@ import $ from 'jquery' import { emit } from '@nextcloud/event-bus' +import { loadState } from '@nextcloud/initial-state' +import { getCurrentUser } from '@nextcloud/auth' import { generateUrl } from './OC/routing' import OC from './OC' -import { setToken as setRequestToken } from './OC/requesttoken' +import { setToken as setRequestToken, getToken as getRequestToken } from './OC/requesttoken' + +let config = null +/** + * The legacy jsunit tests overwrite OC.config before calling initCore + * therefore we need to wait with assigning the config fallback until initCore calls initSessionHeartBeat + */ +const loadConfig = () => { + try { + config = loadState('core', 'config') + } catch (e) { + // This fallback is just for our legacy jsunit tests since we have no way to mock loadState calls + config = OC.config + } +} /** * session heartbeat (defaults to enabled) * @returns {boolean} */ const keepSessionAlive = () => { - return OC.config.session_keepalive === undefined - || !!OC.config.session_keepalive + return config.session_keepalive === undefined + || !!config.session_keepalive } /** @@ -41,8 +57,8 @@ const keepSessionAlive = () => { */ const getInterval = () => { let interval = NaN - if (OC.config.session_lifetime) { - interval = Math.floor(OC.config.session_lifetime / 2) + if (config.session_lifetime) { + interval = Math.floor(config.session_lifetime / 2) } // minimum one minute, max 24 hours, default 15 minutes @@ -83,11 +99,48 @@ const startPolling = () => { return interval } +const registerAutoLogout = () => { + if (!config.auto_logout || !getCurrentUser()) { + return + } + + let lastActive = Date.now() + window.addEventListener('mousemove', e => { + lastActive = Date.now() + localStorage.setItem('lastActive', lastActive) + }) + + window.addEventListener('touchstart', e => { + lastActive = Date.now() + localStorage.setItem('lastActive', lastActive) + }) + + window.addEventListener('storage', e => { + if (e.key !== 'lastActive') { + return + } + lastActive = e.newValue + }) + + setInterval(function() { + const timeout = Date.now() - config.session_lifetime * 1000 + if (lastActive < timeout) { + console.info('Inactivity timout reached, logging out') + const logoutUrl = generateUrl('/logout') + '?requesttoken=' + getRequestToken() + window.location = logoutUrl + } + }, 1000) +} + /** * Calls the server periodically to ensure that session and CSRF * token doesn't expire */ export const initSessionHeartBeat = () => { + loadConfig() + + registerAutoLogout() + if (!keepSessionAlive()) { console.info('session heartbeat disabled') return diff --git a/lib/private/Authentication/Login/FinishRememberedLoginCommand.php b/lib/private/Authentication/Login/FinishRememberedLoginCommand.php index 1d33f103fdf5d2e41ebab59749e9b220f671aafc..8f60c893ec5ef3cdcc8955c41a9ecdd4ca937ee0 100644 --- a/lib/private/Authentication/Login/FinishRememberedLoginCommand.php +++ b/lib/private/Authentication/Login/FinishRememberedLoginCommand.php @@ -26,18 +26,22 @@ declare(strict_types=1); namespace OC\Authentication\Login; use OC\User\Session; +use OCP\IConfig; class FinishRememberedLoginCommand extends ALoginCommand { /** @var Session */ private $userSession; + /** @var IConfig */ + private $config; - public function __construct(Session $userSession) { + public function __construct(Session $userSession, IConfig $config) { $this->userSession = $userSession; + $this->config = $config; } public function process(LoginData $loginData): LoginResult { - if ($loginData->isRememberLogin()) { + if ($loginData->isRememberLogin() && $this->config->getSystemValue('auto_logout', false) === false) { $this->userSession->createRememberMeToken($loginData->getUser()); } diff --git a/lib/private/Template/JSConfigHelper.php b/lib/private/Template/JSConfigHelper.php index 70d6f73628d66bbe7a9ea66c55c4546410a731b7..49cf5f46d81f1ae5ca08c98af28a03f579905c59 100644 --- a/lib/private/Template/JSConfigHelper.php +++ b/lib/private/Template/JSConfigHelper.php @@ -37,6 +37,7 @@ use OCP\App\IAppManager; use OCP\Defaults; use OCP\IConfig; use OCP\IGroupManager; +use OCP\IInitialStateService; use OCP\IL10N; use OCP\ISession; use OCP\IURLGenerator; @@ -75,6 +76,9 @@ class JSConfigHelper { /** @var CapabilitiesManager */ private $capabilitiesManager; + /** @var IInitialStateService */ + private $initialStateService; + /** @var array user back-ends excluded from password verification */ private $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true]; @@ -99,7 +103,8 @@ class JSConfigHelper { IGroupManager $groupManager, IniGetWrapper $iniWrapper, IURLGenerator $urlGenerator, - CapabilitiesManager $capabilitiesManager) { + CapabilitiesManager $capabilitiesManager, + IInitialStateService $initialStateService) { $this->l = $l; $this->defaults = $defaults; $this->appManager = $appManager; @@ -110,6 +115,7 @@ class JSConfigHelper { $this->iniWrapper = $iniWrapper; $this->urlGenerator = $urlGenerator; $this->capabilitiesManager = $capabilitiesManager; + $this->initialStateService = $initialStateService; } public function getConfig() { @@ -146,7 +152,7 @@ class JSConfigHelper { $defaultExpireDateEnabled = $this->config->getAppValue('core', 'shareapi_default_expire_date', 'no') === 'yes'; $defaultExpireDate = $enforceDefaultExpireDate = null; if ($defaultExpireDateEnabled) { - $defaultExpireDate = (int) $this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7'); + $defaultExpireDate = (int)$this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7'); $enforceDefaultExpireDate = $this->config->getAppValue('core', 'shareapi_enforce_expire_date', 'no') === 'yes'; } $outgoingServer2serverShareEnabled = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'yes'; @@ -154,12 +160,12 @@ class JSConfigHelper { $defaultInternalExpireDateEnabled = $this->config->getAppValue('core', 'shareapi_default_internal_expire_date', 'no') === 'yes'; $defaultInternalExpireDate = $defaultInternalExpireDateEnforced = null; if ($defaultInternalExpireDateEnabled) { - $defaultInternalExpireDate = (int) $this->config->getAppValue('core', 'shareapi_internal_expire_after_n_days', '7'); + $defaultInternalExpireDate = (int)$this->config->getAppValue('core', 'shareapi_internal_expire_after_n_days', '7'); $defaultInternalExpireDateEnforced = $this->config->getAppValue('core', 'shareapi_internal_enforce_expire_date', 'no') === 'yes'; } $countOfDataLocation = 0; - $dataLocation = str_replace(\OC::$SERVERROOT .'/', '', $this->config->getSystemValue('datadirectory', ''), $countOfDataLocation); + $dataLocation = str_replace(\OC::$SERVERROOT . '/', '', $this->config->getSystemValue('datadirectory', ''), $countOfDataLocation); if ($countOfDataLocation !== 1 || !$this->groupManager->isAdmin($uid)) { $dataLocation = false; } @@ -175,17 +181,31 @@ class JSConfigHelper { $capabilities = $this->capabilitiesManager->getCapabilities(); + $config = [ + 'session_lifetime' => min($this->config->getSystemValue('session_lifetime', $this->iniWrapper->getNumeric('session.gc_maxlifetime')), $this->iniWrapper->getNumeric('session.gc_maxlifetime')), + 'session_keepalive' => $this->config->getSystemValue('session_keepalive', true), + 'auto_logout' => $this->config->getSystemValue('auto_logout', false), + 'version' => implode('.', \OCP\Util::getVersion()), + 'versionstring' => \OC_Util::getVersionString(), + 'enable_avatars' => true, // here for legacy reasons - to not crash existing code that relies on this value + 'lost_password_link' => $this->config->getSystemValue('lost_password_link', null), + 'modRewriteWorking' => $this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true', + 'sharing.maxAutocompleteResults' => (int)$this->config->getSystemValue('sharing.maxAutocompleteResults', 0), + 'sharing.minSearchStringLength' => (int)$this->config->getSystemValue('sharing.minSearchStringLength', 0), + 'blacklist_files_regex' => \OCP\Files\FileInfo::BLACKLIST_FILES_REGEX, + ]; + $array = [ "_oc_debug" => $this->config->getSystemValue('debug', false) ? 'true' : 'false', "_oc_isadmin" => $this->groupManager->isAdmin($uid) ? 'true' : 'false', "backendAllowsPasswordConfirmation" => $userBackendAllowsPasswordConfirmation ? 'true' : 'false', - "oc_dataURL" => is_string($dataLocation) ? "\"".$dataLocation."\"" : 'false', - "_oc_webroot" => "\"".\OC::$WEBROOT."\"", - "_oc_appswebroots" => str_replace('\\/', '/', json_encode($apps_paths)), // Ugly unescape slashes waiting for better solution + "oc_dataURL" => is_string($dataLocation) ? "\"" . $dataLocation . "\"" : 'false', + "_oc_webroot" => "\"" . \OC::$WEBROOT . "\"", + "_oc_appswebroots" => str_replace('\\/', '/', json_encode($apps_paths)), // Ugly unescape slashes waiting for better solution "datepickerFormatDate" => json_encode($this->l->l('jsdate', null)), 'nc_lastLogin' => $lastConfirmTimestamp, 'nc_pageLoad' => time(), - "dayNames" => json_encode([ + "dayNames" => json_encode([ (string)$this->l->t('Sunday'), (string)$this->l->t('Monday'), (string)$this->l->t('Tuesday'), @@ -194,7 +214,7 @@ class JSConfigHelper { (string)$this->l->t('Friday'), (string)$this->l->t('Saturday') ]), - "dayNamesShort" => json_encode([ + "dayNamesShort" => json_encode([ (string)$this->l->t('Sun.'), (string)$this->l->t('Mon.'), (string)$this->l->t('Tue.'), @@ -203,7 +223,7 @@ class JSConfigHelper { (string)$this->l->t('Fri.'), (string)$this->l->t('Sat.') ]), - "dayNamesMin" => json_encode([ + "dayNamesMin" => json_encode([ (string)$this->l->t('Su'), (string)$this->l->t('Mo'), (string)$this->l->t('Tu'), @@ -240,19 +260,8 @@ class JSConfigHelper { (string)$this->l->t('Nov.'), (string)$this->l->t('Dec.') ]), - "firstDay" => json_encode($this->l->l('firstday', null)) , - "_oc_config" => json_encode([ - 'session_lifetime' => min($this->config->getSystemValue('session_lifetime', $this->iniWrapper->getNumeric('session.gc_maxlifetime')), $this->iniWrapper->getNumeric('session.gc_maxlifetime')), - 'session_keepalive' => $this->config->getSystemValue('session_keepalive', true), - 'version' => implode('.', \OCP\Util::getVersion()), - 'versionstring' => \OC_Util::getVersionString(), - 'enable_avatars' => true, // here for legacy reasons - to not crash existing code that relies on this value - 'lost_password_link'=> $this->config->getSystemValue('lost_password_link', null), - 'modRewriteWorking' => $this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true', - 'sharing.maxAutocompleteResults' => (int)$this->config->getSystemValue('sharing.maxAutocompleteResults', 0), - 'sharing.minSearchStringLength' => (int)$this->config->getSystemValue('sharing.minSearchStringLength', 0), - 'blacklist_files_regex' => \OCP\Files\FileInfo::BLACKLIST_FILES_REGEX, - ]), + "firstDay" => json_encode($this->l->l('firstday', null)), + "_oc_config" => json_encode($config), "oc_appconfig" => json_encode([ 'core' => [ 'defaultExpireDateEnabled' => $defaultExpireDateEnabled, @@ -296,6 +305,8 @@ class JSConfigHelper { ]); } + $this->initialStateService->provideInitialState('core', 'config', $config); + // Allow hooks to modify the output values \OC_Hook::emit('\OCP\Config', 'js', ['array' => &$array]); diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index ddddb8704c82d6ccfcd37260f0c43132eb6b133e..1fbf0acb99c1f29def66df176cd9a89b3aaf11bc 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -49,6 +49,7 @@ use OC\Template\JSCombiner; use OC\Template\JSConfigHelper; use OC\Template\SCSSCacher; use OCP\Defaults; +use OCP\IInitialStateService; use OCP\Support\Subscription\IRegistry; class TemplateLayout extends \OC_Template { @@ -183,7 +184,8 @@ class TemplateLayout extends \OC_Template { \OC::$server->getGroupManager(), \OC::$server->getIniWrapper(), \OC::$server->getURLGenerator(), - \OC::$server->getCapabilitiesManager() + \OC::$server->getCapabilitiesManager(), + \OC::$server->query(IInitialStateService::class) ); $this->assign('inline_ocjs', $jsConfigHelper->getConfig()); } else { diff --git a/tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php b/tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php index 98df129771a5a44224852bc64e4a0d8567464e27..7b4612194563c2af4d19233aa87a957d9ddfcf2d 100644 --- a/tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php +++ b/tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php @@ -27,20 +27,25 @@ namespace lib\Authentication\Login; use OC\Authentication\Login\FinishRememberedLoginCommand; use OC\User\Session; +use OCP\IConfig; use PHPUnit\Framework\MockObject\MockObject; class FinishRememberedLoginCommandTest extends ALoginCommandTest { /** @var Session|MockObject */ private $userSession; + /** @var IConfig|MockObject */ + private $config; protected function setUp(): void { parent::setUp(); $this->userSession = $this->createMock(Session::class); + $this->config = $this->createMock(IConfig::class); $this->cmd = new FinishRememberedLoginCommand( - $this->userSession + $this->userSession, + $this->config ); } @@ -57,6 +62,10 @@ class FinishRememberedLoginCommandTest extends ALoginCommandTest { public function testProcess() { $data = $this->getLoggedInLoginData(); + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('auto_logout', false) + ->willReturn(false); $this->userSession->expects($this->once()) ->method('createRememberMeToken') ->with($this->user); @@ -65,4 +74,18 @@ class FinishRememberedLoginCommandTest extends ALoginCommandTest { $this->assertTrue($result->isSuccess()); } + + public function testProcessNotRemeberedLoginWithAutologout() { + $data = $this->getLoggedInLoginData(); + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('auto_logout', false) + ->willReturn(true); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } }