Merge pull request #20298 from nextcloud/enh/noid/auto-logout

Allow to configure auto logout after browser inactivity
This commit is contained in:
blizzz 2020-04-24 13:20:27 +02:00 committed by GitHub
commit d458fcddb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 219 additions and 117 deletions

View file

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

26
core/js/dist/login.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

98
core/js/dist/main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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

View file

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

View file

@ -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]);

View file

@ -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 {

View file

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