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());
+	}
 }