Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to configure auto logout after browser inactivity #20298

Merged
merged 6 commits into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions config/config.sample.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,15 @@
*/
'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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably make sure that if this is set to true we don't generate a remember me cookie

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6052f82 should to the trick, please have a look


/**
* Enforce token authentication for clients, which blocks requests using the user
* password for enhanced security. Users need to generate tokens in personal settings
Expand Down
10 changes: 5 additions & 5 deletions core/js/dist/install.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion core/js/dist/install.js.map

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions core/js/dist/login.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion core/js/dist/login.js.map

Large diffs are not rendered by default.

98 changes: 49 additions & 49 deletions core/js/dist/main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion core/js/dist/main.js.map

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions core/js/dist/maintenance.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion core/js/dist/maintenance.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions core/js/dist/recommendedapps.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion core/js/dist/recommendedapps.js.map

Large diffs are not rendered by default.

63 changes: 58 additions & 5 deletions core/src/session-heartbeat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -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
ChristophWurst marked this conversation as resolved.
Show resolved Hide resolved
})

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,22 @@
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());
}

Expand Down
57 changes: 34 additions & 23 deletions lib/private/Template/JSConfigHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
use OCP\Defaults;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IInitialStateService;
use OCP\IL10N;
use OCP\ISession;
use OCP\IURLGenerator;
Expand Down Expand Up @@ -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];

Expand All @@ -99,7 +103,8 @@ public function __construct(IL10N $l,
IGroupManager $groupManager,
IniGetWrapper $iniWrapper,
IURLGenerator $urlGenerator,
CapabilitiesManager $capabilitiesManager) {
CapabilitiesManager $capabilitiesManager,
IInitialStateService $initialStateService) {
$this->l = $l;
$this->defaults = $defaults;
$this->appManager = $appManager;
Expand All @@ -110,6 +115,7 @@ public function __construct(IL10N $l,
$this->iniWrapper = $iniWrapper;
$this->urlGenerator = $urlGenerator;
$this->capabilitiesManager = $capabilitiesManager;
$this->initialStateService = $initialStateService;
}

public function getConfig() {
Expand Down Expand Up @@ -146,20 +152,20 @@ public function getConfig() {
$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';

$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;
}
Expand All @@ -175,17 +181,31 @@ public function getConfig() {

$capabilities = $this->capabilitiesManager->getCapabilities();

$config = [
juliusknorr marked this conversation as resolved.
Show resolved Hide resolved
'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'),
Expand All @@ -194,7 +214,7 @@ public function getConfig() {
(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.'),
Expand All @@ -203,7 +223,7 @@ public function getConfig() {
(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'),
Expand Down Expand Up @@ -240,19 +260,8 @@ public function getConfig() {
(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,
Expand Down Expand Up @@ -296,6 +305,8 @@ public function getConfig() {
]);
}

$this->initialStateService->provideInitialState('core', 'config', $config);

// Allow hooks to modify the output values
\OC_Hook::emit('\OCP\Config', 'js', ['array' => &$array]);

Expand Down
4 changes: 3 additions & 1 deletion lib/private/TemplateLayout.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
use OC\Template\JSConfigHelper;
use OC\Template\SCSSCacher;
use OCP\Defaults;
use OCP\IInitialStateService;
use OCP\Support\Subscription\IRegistry;

class TemplateLayout extends \OC_Template {
Expand Down Expand Up @@ -183,7 +184,8 @@ public function __construct($renderAs, $appId = '') {
\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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,25 @@

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

Expand All @@ -57,6 +62,10 @@ public function testProcessNotRememberedLogin() {

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);
Expand All @@ -65,4 +74,18 @@ public function testProcess() {

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