Skip to content

Commit ce56fd9

Browse files
committed
Session lifetime extension
aka Persisted Login plugin functionality in core code. Allows admins to set `$config['session_lifetime_extension_days']`, which allows users to switch on an extended session lifetime in the login form. In effect, these user sessions are valid for the configured number of days after the last activity, even across network outages, closed browsers (as long as they keep their cookies), etc.
1 parent 59700c5 commit ce56fd9

File tree

7 files changed

+65
-4
lines changed

7 files changed

+65
-4
lines changed

config/defaults.inc.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,15 @@
646646
// Session lifetime in minutes
647647
$config['session_lifetime'] = 10;
648648

649+
// Allow users to extend their session lifetime to up to X days by checking a
650+
// checkbox at the login. Practically this means that a login will survive
651+
// network changes, browser restarts (unless they delete cookies), etc, for up
652+
// to X days without activity.
653+
// Warning: This reduces the effectiveness of Roundcube's session highjacking
654+
// mitigation, since a stolen session cookie will be valid for much longer than
655+
// without this option.
656+
$config['session_lifetime_extension_days'] = 1;
657+
649658
// Session domain: .example.org
650659
$config['session_domain'] = '';
651660

program/include/rcmail_output_html.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2355,6 +2355,18 @@ protected function login_form($attrib)
23552355
'buttons' => [],
23562356
];
23572357

2358+
if ($this->config->session_lifetime_extension_days() > 0) {
2359+
$session_lifetime_extension_hidden_field = new html_hiddenfield(['name' => '_session_lifetime_extension', 'value' => '0']);
2360+
$form_content['hidden']['session_lifetime_extension'] = $session_lifetime_extension_hidden_field->show();
2361+
2362+
// Make sure the value is in the range 1..365.
2363+
$session_lifetime_extension_text = str_replace('#', $this->config->session_lifetime_extension_days(), $this->app->gettext('session_lifetime_extension_switch_text'));
2364+
$session_lifetime_extension_checkbox = new html_checkbox(['name' => '_session_lifetime_extension', 'id' => '_session_lifetime_extension', 'title' => $session_lifetime_extension_text]);
2365+
$form_content['inputs']['session_lifetime_extension'] = [
2366+
'content' => html::label(['for' => '_session_lifetime_extension'], [$session_lifetime_extension_checkbox->show(), $session_lifetime_extension_text]),
2367+
];
2368+
}
2369+
23582370
if (is_array($default_host) && count($default_host) > 1) {
23592371
$input_host = new html_select(['name' => '_host', 'id' => 'rcmloginhost', 'class' => 'custom-select']);
23602372

program/lib/Roundcube/rcube_config.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class rcube_config
3434
private $userprefs = [];
3535
private $immutable = [];
3636
private $client_tz;
37+
private $session_lifetime_extension_days;
3738

3839
/**
3940
* Renamed options
@@ -926,4 +927,18 @@ public static function resolve_timezone_alias($tzname)
926927

927928
return $deprecated_timezones[$tzname] ?? $tzname;
928929
}
930+
931+
public function session_lifetime_extension_days(): int
932+
{
933+
if ($this->session_lifetime_extension_days === null) {
934+
$config_value = $this->get('session_lifetime_extension_days', 0);
935+
if (is_int($config_value) && $config_value > 0) {
936+
// Make sure the value is in the range 1..365.
937+
$this->session_lifetime_extension_days = min(max(1, $config_value), 365);
938+
} else {
939+
$this->session_lifetime_extension_days = 0;
940+
}
941+
}
942+
return $this->session_lifetime_extension_days;
943+
}
929944
}

program/lib/Roundcube/rcube_session.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,11 @@ public function check_auth()
707707
$this->log('IP check failed for ' . $this->key . '; expected ' . $this->ip . '; got ' . rcube_utils::remote_addr());
708708
}
709709

710+
// Use the lifetime from the session so the cookie-name matching succeeds.
711+
if ($_SESSION['session_lifetime_extension']) {
712+
$this->set_lifetime($_SESSION['session_lifetime_extension']);
713+
}
714+
710715
if ($result && $this->mkcookie($this->now) != $this->cookie) {
711716
$this->log('Session auth check failed for ' . $this->key . '; timeslot = ' . date('Y-m-d H:i:s', $this->now));
712717
$result = false;
@@ -725,6 +730,10 @@ public function check_auth()
725730
if (!$result) {
726731
$this->log('Session authentication failed for ' . $this->key
727732
. '; invalid auth cookie sent; timeslot = ' . date('Y-m-d H:i:s', $prev));
733+
} else {
734+
// Re-set the auth- and session-id-cookie, because in case of an extended session lifetime they can have an
735+
// expiry date in the browser, which we need to extend.
736+
$this->set_auth_cookie();
728737
}
729738

730739
return $result;
@@ -733,10 +742,20 @@ public function check_auth()
733742
/**
734743
* Set session authentication cookie
735744
*/
736-
public function set_auth_cookie()
745+
public function set_auth_cookie(bool $session_lifetime_extension = false): void
737746
{
747+
if ($session_lifetime_extension === true) {
748+
if ($this->config->session_lifetime_extension_days() > 0) {
749+
$lifetime_seconds = $this->config->session_lifetime_extension_days() * 24 * 60 * 60;
750+
$this->set_lifetime($lifetime_seconds);
751+
$_SESSION['session_lifetime_extension'] = $lifetime_seconds;
752+
$cookie_expiry = time() + $lifetime_seconds;
753+
// Set the sessid-cookie (again) to force/renew its expiration date.
754+
rcube_utils::setcookie(ini_get('session.name'), session_id(), $cookie_expiry);
755+
}
756+
}
738757
$this->cookie = $this->mkcookie($this->now);
739-
rcube_utils::setcookie($this->cookiename, $this->cookie, 0);
758+
rcube_utils::setcookie($this->cookiename, $this->cookie, $cookie_expiry ?? 0);
740759
$_COOKIE[$this->cookiename] = $this->cookie;
741760
}
742761

program/localization/en_US/labels.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ $labels['password'] = 'Password';
2121
$labels['server'] = 'Server';
2222
$labels['login'] = 'Login';
2323
$labels['oauthlogin'] = 'Login with $provider';
24+
$labels['session_lifetime_extension_switch_text'] = 'Remember login for up to # days';
2425

2526
// taskbar
2627
$labels['menu'] = 'Menu';

public_html/index.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@
123123
$RCMAIL->session->remove('temp');
124124
$RCMAIL->session->regenerate_id(false);
125125

126-
// send auth cookie if necessary
127-
$RCMAIL->session->set_auth_cookie();
126+
$session_lifetime_extension = rcube_utils::get_input_string('_session_lifetime_extension', rcube_utils::INPUT_POST);
127+
$RCMAIL->session->set_auth_cookie($session_lifetime_extension === 'on');
128128

129129
// log successful login
130130
$RCMAIL->log_login();

skins/elastic/ui.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,11 @@ function rcube_elastic_ui() {
11601160
icon_name = input.data('icon'),
11611161
icon = $('<i>').attr('class', 'input-group-text icon ' + input.attr('name').replace('_', ''));
11621162

1163+
// Ignore checkboxes, they are prettified well enough by pretty_checkbox() already.
1164+
if (input.attr('type') === 'checkbox') {
1165+
return;
1166+
}
1167+
11631168
if (icon_name) {
11641169
icon.addClass(icon_name);
11651170
}

0 commit comments

Comments
 (0)