* Login: RememberMe token for either automatic login or as 2. factor for 2-Factor-Auth

This commit is contained in:
Ralf Becker 2019-08-03 18:37:10 +02:00
parent e9215fa805
commit 2776d215e2
14 changed files with 478 additions and 172 deletions

View File

@ -156,6 +156,9 @@
<column/> <column/>
</columns> </columns>
<rows> <rows>
<row>
<description value="2-Factor-Authentication" span="all" class="subHeader"/>
</row>
<row> <row>
<description value="2-Factor-Authentication for interactive login" label="%s:"/> <description value="2-Factor-Authentication for interactive login" label="%s:"/>
<select id="newsettings[2fa_required]"> <select id="newsettings[2fa_required]">
@ -166,15 +169,57 @@
</select> </select>
</row> </row>
<row> <row>
<description value="Cookie path (allows multiple eGW sessions with different directories, has problemes with SiteMgr!)" label="%s:"/> <vbox>
<select id="newsettings[cookiepath]"> <description value="Allow user to set 'Remember me' token" label="%s:"/>
<option value="">Document root (default)</option> <description value="Requires 'OpenID / OAuth2 Server' app." label="(%s)"/>
<option value="egroupware">EGroupware directory</option> </vbox>
</select> <vbox>
<select id="newsettings[remember_me_token]">
<option value="">allowed just as second factor</option>
<option value="always">direct login without password or second factor</option>
<option value="disabled">disabled, do not show on login page</option>
</select>
<description value="If disabled existing tokens immediatly stop working." label="(%s)"/>
</vbox>
</row> </row>
<row> <row>
<description value="Cookie domain (default empty means use full domain name, for SiteMgr eg. &quot;.domain.com&quot; allows to use the same cookie for egw.domain.com and www.domain.com)" label="%s:"/> <description value="Lifetime of 'Remember me' token" label="%s:"/>
<textbox id="newsettings[cookiedomain]"/> <select id="newsettings[remember_me_lifetime]">
<option value="">{default of currently} {1 month}</option>
<option value="P1W">1 week</option>
<option value="P2W">2 weeks</option>
<option value="P1M">1 month</option>
<option value="P2M">2 month</option>
<option value="P3M">3 month</option>
<option value="P6M">6 month</option>
<option value="P1Y">1 year</option>
<option value="user">User choice</option>
</select>
</row>
<row>
<description value="Blocking after wrong password" span="all" class="subHeader"/>
</row>
<row>
<description value="After how many unsuccessful attempts to login, an account should be blocked (default 3) ?" label="%s:"/>
<textbox id="newsettings[num_unsuccessful_id]" size="5"/>
</row>
<row>
<description value="After how many unsuccessful attempts to login, an IP should be blocked (default 15) ?" label="%s:"/>
<textbox id="newsettings[num_unsuccessful_ip]" size="5"/>
</row>
<row>
<description value="Comma-separated IP addresses white-listed from above blocking (:optional number of attempts)"/>
<textbox id="newsettings[unsuccessful_ip_whitelist]" size="64" blur="X.X.X.X[:N], ..."
validator="/^(\\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?,? *)*$/"/>
</row>
<row>
<description value="How many minutes should an account or IP be blocked (default 1) ?" label="%s:"/>
<textbox id="newsettings[block_time]" size="5"/>
</row>
<row>
<description value="Sessions" span="all" class="subHeader"/>
</row> </row>
<row> <row>
<vbox> <vbox>
@ -195,48 +240,19 @@
</select> </select>
</row> </row>
<row> <row>
<description value="Deny all users access to grant other users access to their entries ?" label="%s:"/> <description value="Cookie path (allows multiple eGW sessions with different directories, has problemes with SiteMgr!)" label="%s:"/>
<select id="newsettings[deny_user_grants_access]"> <select id="newsettings[cookiepath]">
<option value="">No</option> <option value="">Document root (default)</option>
<option value="True">Yes</option> <option value="egroupware">EGroupware directory</option>
</select> </select>
</row> </row>
<!--
<row> <row>
<description value="Default file system space per user"/> <description value="Cookie domain (default empty means use full domain name, for SiteMgr eg. &quot;.domain.com&quot; allows to use the same cookie for egw.domain.com and www.domain.com)" label="%s:"/>
<textbox id="newsettings[vfs_default_account_size_number]" type="text" size="7"/> <textbox id="newsettings[cookiedomain]"/>
</row>
<td>{Default_file_system_space_per_user}/{group_?}:</td>
<td>
<input type="text" name="newsettings[vfs_default_account_size_number]" size="7" value="{value_vfs_default_account_size_number}">&nbsp;&nbsp;
<select name="newsettings[vfs_default_account_size_type]">
<option value="gb"{selected_vfs_default_account_size_type_gb}>GB</option>
<option value="mb"{selected_vfs_default_account_size_type_mb}>MB</option>
<option value="kb"{selected_vfs_default_account_size_type_kb}>KB</option>
<option value="b"{selected_vfs_default_account_size_type_b}>B</option>
</select>
</td>
</row> -->
<row> <row>
<description value="How many days should entries stay in the access log, before they get deleted (default 90) ?" label="%s:"/> <description value="Passwords" span="all" class="subHeader"/>
<textbox id="newsettings[max_access_log_age]" size="5"/>
</row>
<row>
<description value="After how many unsuccessful attempts to login, an account should be blocked (default 3) ?" label="%s:"/>
<textbox id="newsettings[num_unsuccessful_id]" size="5"/>
</row>
<row>
<description value="After how many unsuccessful attempts to login, an IP should be blocked (default 15) ?" label="%s:"/>
<textbox id="newsettings[num_unsuccessful_ip]" size="5"/>
</row>
<row>
<description value="Comma-separated IP addresses white-listed from above blocking (:optional number of attempts)"/>
<textbox id="newsettings[unsuccessful_ip_whitelist]" size="64" blur="X.X.X.X[:N], ..."
validator="/^(\\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?,? *)*$/"/>
</row>
<row>
<description value="How many minutes should an account or IP be blocked (default 1) ?" label="%s:"/>
<textbox id="newsettings[block_time]" size="5"/>
</row> </row>
<row> <row>
<description value="Force users to change their password regularily?(empty for no,number for after that number of days" label="%s:"/> <description value="Force users to change their password regularily?(empty for no,number for after that number of days" label="%s:"/>
@ -278,6 +294,37 @@
<option value="yes">Yes</option> <option value="yes">Yes</option>
</select> </select>
</row> </row>
<row>
<description value="Other security configuration" span="all" class="subHeader"/>
</row>
<row>
<description value="Deny all users access to grant other users access to their entries ?" label="%s:"/>
<select id="newsettings[deny_user_grants_access]">
<option value="">No</option>
<option value="True">Yes</option>
</select>
</row>
<!--
<row>
<description value="Default file system space per user"/>
<textbox id="newsettings[vfs_default_account_size_number]" type="text" size="7"/>
<td>{Default_file_system_space_per_user}/{group_?}:</td>
<td>
<input type="text" name="newsettings[vfs_default_account_size_number]" size="7" value="{value_vfs_default_account_size_number}">&nbsp;&nbsp;
<select name="newsettings[vfs_default_account_size_type]">
<option value="gb"{selected_vfs_default_account_size_type_gb}>GB</option>
<option value="mb"{selected_vfs_default_account_size_type_mb}>MB</option>
<option value="kb"{selected_vfs_default_account_size_type_kb}>KB</option>
<option value="b"{selected_vfs_default_account_size_type_b}>B</option>
</select>
</td>
</row> -->
<row>
<description value="How many days should entries stay in the access log, before they get deleted (default 90) ?" label="%s:"/>
<textbox id="newsettings[max_access_log_age]" size="5"/>
</row>
<row> <row>
<description value="Admin email addresses (comma-separated) to be notified about the blocking (empty for no notify)" label="%s:"/> <description value="Admin email addresses (comma-separated) to be notified about the blocking (empty for no notify)" label="%s:"/>
<textbox id="newsettings[admin_mails]" size="40"/> <textbox id="newsettings[admin_mails]" size="40"/>

View File

@ -418,6 +418,7 @@ distribution lists as groups groupdav de Verteilerlisten als Gruppen
djibouti common de DSCHIBUTI djibouti common de DSCHIBUTI
do not notify common de Nicht benachrichtigen do not notify common de Nicht benachrichtigen
do not notify of these changes common de Es werden keine Benachrichtigungen beim Anlegen oder Ändern versendet. do not notify of these changes common de Es werden keine Benachrichtigungen beim Anlegen oder Ändern versendet.
do not use on public computers! common de NICHT auf öffentlichen Computern verwenden!
do you also want to delete all subcategories ? common de Sollen alle Unterkategorien gelöscht werden ? do you also want to delete all subcategories ? common de Sollen alle Unterkategorien gelöscht werden ?
do you want to save the changes you made in table %s? common de Wollen Sie die Änderungen in der Tabelle '%s' speichern? do you want to save the changes you made in table %s? common de Wollen Sie die Änderungen in der Tabelle '%s' speichern?
do you want to send the message to all selected entries, without further editing? common de Wollen Sie die Nachricht an alle ausgewählten Einträge OHNE weitere Bearbeitung versenden? do you want to send the message to all selected entries, without further editing? common de Wollen Sie die Nachricht an alle ausgewählten Einträge OHNE weitere Bearbeitung versenden?
@ -1082,7 +1083,8 @@ refresh common de Aktualisierung
register common de Registrieren register common de Registrieren
regular common de Normal regular common de Normal
reject common de Zurückweisen reject common de Zurückweisen
remember me common de Mich erinnern remember me common de An mich erinnern
remember me for %1 common de An mich erinnern für %1
remove row (can not be undone!!!) common de löscht eine Zeile (NICHT rückgängig zu machen) remove row (can not be undone!!!) common de löscht eine Zeile (NICHT rückgängig zu machen)
remove selected accounts common de Ausgewählte Benutzer entfernen remove selected accounts common de Ausgewählte Benutzer entfernen
remove shortcut common de Abkürzung entfernen remove shortcut common de Abkürzung entfernen

View File

@ -418,6 +418,7 @@ distribution lists as groups groupdav en Distribution lists as groups
djibouti common en DJIBOUTI djibouti common en DJIBOUTI
do not notify common en Do not notify do not notify common en Do not notify
do not notify of these changes common en Do not send notifications when creating or changing do not notify of these changes common en Do not send notifications when creating or changing
do not use on public computers! common en Do NOT use on public computers!
do you also want to delete all subcategories ? common en Do you also want to delete all sub categories? do you also want to delete all subcategories ? common en Do you also want to delete all sub categories?
do you want to save the changes you made in table %s? common en Do you want to save the changes you made in table %s? do you want to save the changes you made in table %s? common en Do you want to save the changes you made in table %s?
do you want to send the message to all selected entries, without further editing? common en Do you want to send the message to all selected entries, WITHOUT further editing? do you want to send the message to all selected entries, without further editing? common en Do you want to send the message to all selected entries, WITHOUT further editing?
@ -1083,6 +1084,7 @@ register common en Register
regular common en Regular regular common en Regular
reject common en Reject reject common en Reject
remember me common en Remember me remember me common en Remember me
remember me for %1 common en Remember me for %1
remove row (can not be undone!!!) common en Remove row remove row (can not be undone!!!) common en Remove row
remove selected accounts common en Remove selected accounts remove selected accounts common en Remove selected accounts
remove shortcut common en Remove shortcut remove shortcut common en Remove shortcut

View File

@ -223,18 +223,31 @@ class Login
* and place a time selectbox, how long cookie is valid * * and place a time selectbox, how long cookie is valid *
\********************************************************/ \********************************************************/
if($GLOBALS['egw_info']['server']['allow_cookie_auth']) if ($GLOBALS['egw_info']['server']['remember_me_token'] === 'always' ||
($GLOBALS['egw_info']['server']['2fa_required'] !== 'disabled' &&
$GLOBALS['egw_info']['server']['remember_me_token'] !== 'disabled'))
{ {
$tmpl->set_block('login_form','remember_me_selection'); $tmpl->set_block('login_form','remember_me_selection');
$tmpl->set_var('lang_remember_me',lang('Remember me')); $help = htmlspecialchars(lang('Do NOT use on public computers!'));
$tmpl->set_var('select_remember_me',Api\Html::select('remember_me', '', array( $tmpl->set_var('lang_remember_me_help', $help);
'' => lang('not'), if ($GLOBALS['egw_info']['server']['remember_me_lifetime'] === 'user')
'1hour' => lang('1 Hour'), {
'1day' => lang('1 Day'), $tmpl->set_var('lang_remember_me', '');
'1week'=> lang('1 Week'), $tmpl->set_var('select_remember_me',Api\Html::select('remember_me', '', array(
'1month' => lang('1 Month'), '' => lang('Do not remember me'),
'forever' => lang('Forever'), 'P1W'=> lang('Remember me for %1', lang('1 Week')),
),true,'tabindex="3"',0,false)); 'P2W'=> lang('Remember me for %1', lang('2 Weeks')),
'P1M' => lang('Remember me for %1', lang('1 Month')),
'P3M' => lang('Remember me for %1', lang('3 Month')),
'P1Y' => lang('Remember me for %1', lang('1 Year')),
), true, 'tabindex="3" title="'.$help.'"', 0, false));
}
else
{
$tmpl->set_var('lang_remember_me',lang('Remember me'));
$tmpl->set_var('select_remember_me',
Api\Html::checkbox('remember_me', false, 'True', ' id="remember_me" tabindex="3" title="'.$help.'"'));
}
} }
else else
{ {

View File

@ -23,6 +23,8 @@ namespace EGroupware\Api;
use PragmaRX\Google2FA; use PragmaRX\Google2FA;
use EGroupware\Api\Mail\Credentials; use EGroupware\Api\Mail\Credentials;
use EGroupware\OpenID;
use League\OAuth2\Server\Exception\OAuthServerException;
/** /**
* Create, verifies or destroys an EGroupware session * Create, verifies or destroys an EGroupware session
@ -75,6 +77,11 @@ class Session
*/ */
const EGW_SESSION_NAME = 'sessionid'; const EGW_SESSION_NAME = 'sessionid';
/**
* Name of cookie with remember me token
*/
const REMEMBER_ME_COOKIE = 'eGW_remember';
/** /**
* current user login (account_lid@domain) * current user login (account_lid@domain)
* *
@ -449,9 +456,10 @@ class Session
* @param boolean $auth_check =true if false, the user is loged in without checking his password (eg. for single sign on), default = true * @param boolean $auth_check =true if false, the user is loged in without checking his password (eg. for single sign on), default = true
* @param boolean $fail_on_forced_password_change =false true: do NOT create session, if password change requested * @param boolean $fail_on_forced_password_change =false true: do NOT create session, if password change requested
* @param string|boolean $check_2fa =false string: 2fa-code to check (only if exists) and fail if wrong, false: do NOT check 2fa * @param string|boolean $check_2fa =false string: 2fa-code to check (only if exists) and fail if wrong, false: do NOT check 2fa
* @param string $remember_me =null "True" for checkbox checked, or periode for user-choice select-box eg. "P1W" or "" for NOT remember
* @return string|boolean session id or false if session was not created, $this->(cd_)reason contains cause * @return string|boolean session id or false if session was not created, $this->(cd_)reason contains cause
*/ */
function create($login,$passwd = '',$passwd_type = '',$no_session=false,$auth_check=true,$fail_on_forced_password_change=false,$check_2fa=false) function create($login,$passwd = '',$passwd_type = '',$no_session=false,$auth_check=true,$fail_on_forced_password_change=false,$check_2fa=false,$remember_me=null)
{ {
try { try {
if (is_array($login)) if (is_array($login))
@ -498,6 +506,12 @@ class Session
$this->account_id = $GLOBALS['egw']->accounts->name2id($this->account_lid,'account_lid','u'); $this->account_id = $GLOBALS['egw']->accounts->name2id($this->account_lid,'account_lid','u');
// do we need to check 'remember me' token (to bypass authentication)
if ($auth_check && !empty($_COOKIE[self::REMEMBER_ME_COOKIE]))
{
$auth_check = !$this->skipPasswordAuth($_COOKIE[self::REMEMBER_ME_COOKIE], $this->account_id);
}
if (($blocked = $this->login_blocked($login,$user_ip)) || // too many unsuccessful attempts if (($blocked = $this->login_blocked($login,$user_ip)) || // too many unsuccessful attempts
$GLOBALS['egw_info']['server']['global_denied_users'][$this->account_lid] || $GLOBALS['egw_info']['server']['global_denied_users'][$this->account_lid] ||
$auth_check && !$GLOBALS['egw']->auth->authenticate($this->account_lid, $this->passwd, $this->passwd_type) || $auth_check && !$GLOBALS['egw']->auth->authenticate($this->account_lid, $this->passwd, $this->passwd_type) ||
@ -564,23 +578,10 @@ class Session
Cache::setSession('phpgwapi', 'password', base64_encode($this->passwd)); Cache::setSession('phpgwapi', 'password', base64_encode($this->passwd));
// if we have a second factor, check it before forced password change // if we have a second factor, check it before forced password change
if ($check_2fa !== false && if ($check_2fa !== false)
$GLOBALS['egw_info']['server']['2fa_required'] !== 'disabled' &&
(($creds = Credentials::read(0, Credentials::TWOFA, $this->account_id)) ||
$GLOBALS['egw_info']['server']['2fa_required'] === 'strict'))
{ {
$google2fa = new Google2FA\Google2FA();
try { try {
if (empty($check_2fa) || empty($creds)) $this->checkMultifactorAuth($check_2fa, $_COOKIE[self::REMEMBER_ME_COOKIE]);
{
throw new \Exception(Framework\Login::check_logoutcode(self::CD_SECOND_FACTOR_REQUIRED), self::CD_SECOND_FACTOR_REQUIRED);
}
if (!$google2fa->verify($check_2fa, $creds['2fa_password']))
{
// we log the missing factor, but externally only show "Bad Login or Password"
// to give no indication that the password was already correct
throw new \Exception('Invalid 2-Factor Authentication code', self::CD_BAD_LOGIN_OR_PASSWORD);
}
} }
catch(\Exception $e) { catch(\Exception $e) {
$this->cd_reason = $e->getCode(); $this->cd_reason = $e->getCode();
@ -649,6 +650,14 @@ class Session
self::egw_setcookie('last_loginid', $this->account_lid ,$now+1209600); /* For 2 weeks */ self::egw_setcookie('last_loginid', $this->account_lid ,$now+1209600); /* For 2 weeks */
self::egw_setcookie('last_domain',$this->account_domain,$now+1209600); self::egw_setcookie('last_domain',$this->account_domain,$now+1209600);
} }
// set new remember me token/cookie, if requested and necessary
$expiration = null;
if (($token = $this->checkSetRememberMeToken($remember_me, $_COOKIE[self::REMEMBER_ME_COOKIE], $expiration)))
{
self::egw_setcookie(self::REMEMBER_ME_COOKIE, $token, $expiration);
}
if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) successfull sessionid=$this->sessionid"); if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) successfull sessionid=$this->sessionid");
// hook called once session is created // hook called once session is created
@ -670,7 +679,9 @@ class Session
} }
// catch all exceptions, as their (allways logged) trace (eg. on a database error) would contain the user password // catch all exceptions, as their (allways logged) trace (eg. on a database error) would contain the user password
catch(Exception $e) { catch(Exception $e) {
$this->reason = $this->cd_reason = $e->getMessage(); $this->reason = $this->cd_reason = is_a($e, Db\Exception::class) ?
// do not output specific database error, eg. invalid SQL statement
lang('Database Error!') : $e->getMessage();
error_log(__METHOD__."('$login', ".array2string(str_repeat('*', strlen($passwd))). error_log(__METHOD__."('$login', ".array2string(str_repeat('*', strlen($passwd))).
", '$passwd_type', no_session=".array2string($no_session). ", '$passwd_type', no_session=".array2string($no_session).
", auth_check=".array2string($auth_check). ", auth_check=".array2string($auth_check).
@ -680,6 +691,246 @@ class Session
} }
} }
/**
* Check if password authentication is required or given token is sufficient
*
* Token is only checked for 'remember_me_token' === 'always', not for default of only for 2FA!
*
* Password auth is also required if 2FA is not disabled and either required or configured by user.
*
* @param string $token value of token
* @param int& $account_id =null account_id of token-owner to limit check on that user, on return account_id of token owner
* @return boolean false: if further auth check is required, true: if token is sufficient for authentication
*/
public function skipPasswordAuth($token, &$account_id=null)
{
// if token is empty or disabled --> password authentication required
if (empty($token) || $GLOBALS['egw_info']['server']['remember_me_token'] !== 'always' ||
!($client = $this->checkOpenIDconfigured()))
{
return false;
}
// check if token exists and is (still) valid
$tokenRepo = new OpenID\Repositories\AccessTokenRepository();
if (!($access_token = $tokenRepo->findToken($client, $account_id, 'PT1S', $token)))
{
return false;
}
$account_id = $access_token->getUserIdentifier();
// check if we need a second factor
if ($GLOBALS['egw_info']['server']['2fa_required'] !== 'disabled' &&
(($creds = Credentials::read(0, Credentials::TWOFA, $account_id)) ||
$GLOBALS['egw_info']['server']['2fa_required'] === 'strict'))
{
return false;
}
// access-token is sufficient
return true;
}
/**
* Check multifcator authemtication
*
* @param string $code 2fa-code
* @param string $token remember me token
* @throws \Exception with error-message if NOT successful
*/
protected function checkMultifactorAuth($code, $token)
{
$errors = $factors = [];
if ($GLOBALS['egw_info']['server']['2fa_required'] === 'disabled')
{
return; // nothing to check
}
// check if token exists and is (still) valid
if (!empty($token) && $GLOBALS['egw_info']['server']['remember_me_token'] !== 'disabled' &&
($client = $this->checkOpenIDconfigured()))
{
$tokenRepo = new OpenID\Repositories\AccessTokenRepository();
if ($tokenRepo->findToken($client, $this->account_id, 'PT1S', $token))
{
$factors['remember_me_token'] = true;
}
else
{
$errors['remember_me_token'] = lang("Invalid or expired 'remember me' token");
}
}
// if 2fa is configured by user, check it
if (($creds = Credentials::read(0, Credentials::TWOFA, $this->account_id)))
{
if (empty($code))
{
$errors['2fa_code'] = lang('2-Factor Authentication code required');
}
else
{
$google2fa = new Google2FA\Google2FA();
if (!empty($code) && $google2fa->verify($code, $creds['2fa_password']))
{
$factors['2fa_code'] = true;
}
else
{
$errors['2fa_code'] = lang('Invalid 2-Factor Authentication code');
}
}
}
// check for more factors and/or policies
// hook can add factors, errors or throw \Exception with error-message and -code
Hooks::process([
'location' => 'multifactor_policy',
'factors' => &$factors,
'errors' => &$errors,
'2fa_code' => $code,
'remember_me_token' => $token,
], [], true);
if (!count($factors) && (isset($errors['2fa_code']) ||
$GLOBALS['egw_info']['server']['2fa_required'] === 'strict'))
{
if (!empty($code) && isset($errors['2fa_code']))
{
// we log the missing factor, but externally only show "Bad Login or Password"
// to give no indication that the password was already correct
throw new \Exception(implode(', ', $errors), self::CD_BAD_LOGIN_OR_PASSWORD);
}
else
{
throw new \Exception(implode(', $errors'), self::CD_SECOND_FACTOR_REQUIRED);
}
}
}
/**
* Check if we need to set a remember me token/cookie
*
* @param string $remember_me =null "True" for checkbox checked, or periode for user-choice select-box eg. "P1W" or "" for NOT remember
* @param string $token current remember me token
* @param int& $expriation on return expiration time of new cookie
* @return string new token to set as Cookieor null to not set a new one
*/
protected function checkSetRememberMeToken($remember_me, $token, &$expiration)
{
// do we need a new token
if (!empty($remember_me) && $GLOBALS['egw_info']['server']['remember_me_token'] !== 'disabled' &&
($client = $this->checkOpenIDconfigured()))
{
if (!empty($token))
{
// check if token exists and is (still) valid
$tokenRepo = new OpenID\Repositories\AccessTokenRepository();
if ($tokenRepo->findToken($client, $this->account_id, 'PT1S', $token))
{
return null; // token still valid, no need to set it again
}
}
$lifetime = $this->rememberMeTokenLifetime(is_string($remember_me) ? $remember_me : null);
$expiration = $this->rememberMeTokenLifetime(is_string($remember_me) ? $remember_me : null, true);
$tokenFactory = new OpenID\Token();
if (($token = $tokenFactory->accessToken(self::OPENID_REMEMBER_ME_CLIENT_ID, [], $lifetime, false, $lifetime, false)))
{
return $token->getIdentifier();
}
}
return null;
}
/**
* Check if 'remember me' token should be deleted on explict logout
*
* @return boolean false: if 2FA is enabeld for user, true: otherwise
*/
public function removeRememberMeTokenOnLogout()
{
return $GLOBALS['egw_info']['server']['2fa_required'] === 'disabled' ||
$GLOBALS['egw_info']['server']['2fa_required'] !== 'strict' &&
!($creds = Credentials::read(0, Credentials::TWOFA, $this->account_id));
}
/**
* OpenID Client ID for remember me token
*/
const OPENID_REMEMBER_ME_CLIENT_ID = 'login-remember-me';
/**
* Check and if not configure OpenID app to generate 'remember me' tokens
*
* @return OpenID\Entities\ClientEntity|null null if OpenID Server app is not installed
*/
protected function checkOpenIDconfigured()
{
// OpenID app not installed --> password authentication required
if (!isset($GLOBALS['egw_info']['apps']))
{
$GLOBALS['egw']->applications->read_installed_apps();
}
if (empty($GLOBALS['egw_info']['apps']['openid']))
{
return null;
}
$clients = new OpenID\Repositories\ClientRepository();
try {
$client = $clients->getClientEntity(self::OPENID_REMEMBER_ME_CLIENT_ID, null, null, false); // false = do NOT check client-secret
}
catch (OAuthServerException $e)
{
unset($e);
$client = new OpenID\Entities\ClientEntity();
$client->setIdentifier(self::OPENID_REMEMBER_ME_CLIENT_ID);
$client->setSecret(Auth::randomstring(24)); // must not be unset
$client->setName(lang('Remember me token'));
$client->setAccessTokenTTL($this->rememberMeTokenLifetime());
$client->setRefreshTokenTTL('P0S'); // no refresh token
$client->setRedirectUri($GLOBALS['egw_info']['server']['webserver_url'].'/');
$clients->persistNewClient($client);
}
return $client;
}
/**
* Return lifetime for remember me token
*
* @param string $user user choice, if allowed
* @param boolean $ts =false false: return periode string, true: return integer timestamp
* @return string periode spec eg. 'P1M'
*/
protected function rememberMeTokenLifetime($user=null, $ts=false)
{
switch ((string)$GLOBALS['egw_info']['server']['remember_me_lifetime'])
{
case 'user':
if (!empty($user))
{
$lifetime = $user;
break;
}
// fall-through for default lifetime
case '': // default lifetime
$lifetime = 'P1M';
break;
default:
$lifetime = $GLOBALS['egw_info']['server']['remember_me_lifetime'];
break;
}
if ($ts)
{
$expiration = new DateTime('now', DateTime::$server_timezone);
$expiration->add(new \DateInterval($lifetime));
return $expiration->format('ts');
}
return $lifetime;
}
/** /**
* Store eGW specific session-vars * Store eGW specific session-vars
* *

104
login.php
View File

@ -136,44 +136,27 @@ else
// some apache mod_auth_* modules use REMOTE_USER instead of PHP_AUTH_USER, thanks to Sylvain Beucler // some apache mod_auth_* modules use REMOTE_USER instead of PHP_AUTH_USER, thanks to Sylvain Beucler
if ($GLOBALS['egw_info']['server']['auth_type'] == 'http' && !isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['REMOTE_USER'])) if ($GLOBALS['egw_info']['server']['auth_type'] == 'http' && !isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['REMOTE_USER']))
{ {
$_SERVER['PHP_AUTH_USER'] = $_SERVER['REMOTE_USER']; $_SERVER['PHP_AUTH_USER'] = $_SERVER['REMOTE_USER'];
} }
if($GLOBALS['egw_info']['server']['auth_type'] == 'http' && isset($_SERVER['PHP_AUTH_USER'])) $passwd = get_magic_quotes_gpc() ? stripslashes($_POST['passwd']) : $_POST['passwd'];
$passwd_type = $_POST['passwd_type'];
// forced password change
if($GLOBALS['egw']->session->cd_reason != Api\Session::CD_FORCE_PASSWORD_CHANGE)
{
// no automatic login
}
// authentication via Apache
elseif ($GLOBALS['egw_info']['server']['auth_type'] == 'http' && isset($_SERVER['PHP_AUTH_USER']))
{ {
$submit = True; $submit = True;
$login = $_SERVER['PHP_AUTH_USER']; $login = $_SERVER['PHP_AUTH_USER'];
$passwd = $_SERVER['PHP_AUTH_PW']; $passwd = $_SERVER['PHP_AUTH_PW'];
$passwd_type = 'text'; $passwd_type = 'text';
} }
else
{
$passwd = get_magic_quotes_gpc() ? stripslashes($_POST['passwd']) : $_POST['passwd'];
$passwd_type = $_POST['passwd_type'];
if($GLOBALS['egw_info']['server']['allow_cookie_auth'])
{
$eGW_remember = explode('::::',get_magic_quotes_gpc() ? stripslashes($_COOKIE['eGW_remember']) : $_COOKIE['eGW_remember']);
if($eGW_remember[0] && $eGW_remember[1] && $eGW_remember[2])
{
$_SERVER['PHP_AUTH_USER'] = $login = $eGW_remember[0];
$_SERVER['PHP_AUTH_PW'] = $passwd = $eGW_remember[1];
$passwd_type = $eGW_remember[2];
$submit = True;
}
}
if(!$passwd && ($GLOBALS['egw_info']['server']['auto_anon_login']) && !$_GET['cd'])
{
$_SERVER['PHP_AUTH_USER'] = $login = 'anonymous';
$_SERVER['PHP_AUTH_PW'] = $passwd = 'anonymous';
$passwd_type = 'text';
$submit = True;
}
}
# Apache + mod_ssl style SSL certificate authentication # Apache + mod_ssl style SSL certificate authentication
# Certificate (chain) verification occurs inside mod_ssl # Certificate (chain) verification occurs inside mod_ssl
if($GLOBALS['egw_info']['server']['auth_type'] == 'sqlssl' && isset($_SERVER['SSL_CLIENT_S_DN']) && !isset($_GET['cd'])) elseif($GLOBALS['egw_info']['server']['auth_type'] == 'sqlssl' && isset($_SERVER['SSL_CLIENT_S_DN']) && !isset($_GET['cd']))
{ {
// an X.509 subject looks like: // an X.509 subject looks like:
// CN=john.doe/OU=Department/O=Company/C=xx/Email=john@comapy.tld/L=City/ // CN=john.doe/OU=Department/O=Company/C=xx/Email=john@comapy.tld/L=City/
@ -203,20 +186,33 @@ else
unset($val); unset($val);
unset($sslattributes); unset($sslattributes);
} }
else
if(isset($passwd_type) || $_POST['submitit_x'] || $_POST['submitit_y'] || $submit)
{ {
if(getenv('REQUEST_METHOD') != 'POST' && $_SERVER['REQUEST_METHOD'] != 'POST' && // check if we have a sufficient access-token as cookie and no forced password change
if ($GLOBALS['egw']->session->cd_reason != Api\Session::CD_FORCE_PASSWORD_CHANGE &&
$GLOBALS['egw']->session->skipPasswordAuth($_COOKIE[Api\Session::REMEMBER_ME_COOKIE], $account_id))
{
$_SERVER['PHP_AUTH_USER'] = $login = Api\Accounts::id2name($account_id);
$submit = true;
}
if(!$passwd && ($GLOBALS['egw_info']['server']['auto_anon_login']) && !$_GET['cd'])
{
$_SERVER['PHP_AUTH_USER'] = $login = 'anonymous';
$_SERVER['PHP_AUTH_PW'] = $passwd = 'anonymous';
$passwd_type = 'text';
$submit = True;
}
}
if (isset($passwd_type) || $submit)
{
if($_SERVER['REQUEST_METHOD'] != 'POST' &&
!isset($_SERVER['PHP_AUTH_USER']) && !isset($_SERVER['SSL_CLIENT_S_DN'])) !isset($_SERVER['PHP_AUTH_USER']) && !isset($_SERVER['SSL_CLIENT_S_DN']))
{ {
$GLOBALS['egw']->session->egw_setcookie('eGW_remember','',0,'/');
Egw::redirect_link('/login.php','cd=5'); Egw::redirect_link('/login.php','cd=5');
} }
/* cookie enabled check comment out, as it seems to cause a redirect loop under certain conditions and browsers :-(
if ($_COOKIE['eGW_cookie_test'] !== 'enabled')
{
Egw::redirect_link('/login.php','cd=4');
}*/
// don't get login data again when $submit is true // don't get login data again when $submit is true
if($submit == false) if($submit == false)
@ -252,7 +248,7 @@ else
} }
} }
$GLOBALS['sessionid'] = $GLOBALS['egw']->session->create($login, $passwd, $GLOBALS['sessionid'] = $GLOBALS['egw']->session->create($login, $passwd,
$passwd_type, false, true, true, $_POST['2fa_code']); // true = let session fail on forced password change $passwd_type, false, true, true, $_POST['2fa_code'], $_POST['remember_me']); // true = let session fail on forced password change
if (!$GLOBALS['sessionid'] && $GLOBALS['egw']->session->cd_reason == Api\Session::CD_FORCE_PASSWORD_CHANGE) if (!$GLOBALS['sessionid'] && $GLOBALS['egw']->session->cd_reason == Api\Session::CD_FORCE_PASSWORD_CHANGE)
{ {
@ -278,40 +274,10 @@ else
} }
elseif (!isset($GLOBALS['sessionid']) || ! $GLOBALS['sessionid']) elseif (!isset($GLOBALS['sessionid']) || ! $GLOBALS['sessionid'])
{ {
Api\Session::egw_setcookie('eGW_remember','',0,'/');
Egw::redirect_link('/login.php?cd=' . $GLOBALS['egw']->session->cd_reason); Egw::redirect_link('/login.php?cd=' . $GLOBALS['egw']->session->cd_reason);
} }
else else
{ {
/* set auth_cookie */
if($GLOBALS['egw_info']['server']['allow_cookie_auth'] && $_POST['remember_me'] && $_POST['passwd'])
{
switch ($_POST['remember_me'])
{
case '1hour' :
$remember_time = time()+60*60;
break;
case '1day' :
$remember_time = time()+60*60*24;
break;
case '1week' :
$remember_time = time()+60*60*24*7;
break;
case '1month' :
$remember_time = time()+60*60*24*30;
break;
case 'forever' :
default:
$remember_time = 2147483647;
break;
}
$GLOBALS['egw']->session->egw_setcookie('eGW_remember',implode('::::',array(
'login' => $login,
'passwd' => $passwd,
'passwd_type' => $passwd_type)),
$remember_time,'/'); // make the cookie valid for the whole site (incl. sitemgr) and not only the eGW install-dir
}
if ($_POST['lang'] && preg_match('/^[a-z]{2}(-[a-z]{2})?$/',$_POST['lang']) && if ($_POST['lang'] && preg_match('/^[a-z]{2}(-[a-z]{2})?$/',$_POST['lang']) &&
$_POST['lang'] != $GLOBALS['egw_info']['user']['preferences']['common']['lang']) $_POST['lang'] != $GLOBALS['egw_info']['user']['preferences']['common']['lang'])
{ {

View File

@ -37,13 +37,18 @@ elseif(strpos($redirectTarget, '[?&]cd=') !== false)
$redirectTarget = preg_replace('/([?&])cd=[^&]+/', '$1cd=1', $redirectTarget); $redirectTarget = preg_replace('/([?&])cd=[^&]+/', '$1cd=1', $redirectTarget);
} }
// remove remember me cookie on explicit logout, unless it is a second factor
if ($GLOBALS['egw']->session->removeRememberMeTokenOnLogout())
{
Api\Session::egw_setcookie('eGW_remember','',0,'/');
}
if($verified) if($verified)
{ {
Api\Hooks::process('logout'); Api\Hooks::process('logout');
$GLOBALS['egw']->session->destroy($GLOBALS['sessionid'],$GLOBALS['kp3']); $GLOBALS['egw']->session->destroy($GLOBALS['sessionid'],$GLOBALS['kp3']);
} }
Api\Session::egw_setcookie('eGW_remember','',0,'/');
Api\Session::egw_setcookie('sessionid'); Api\Session::egw_setcookie('sessionid');
Api\Session::egw_setcookie('kp3'); Api\Session::egw_setcookie('kp3');
Api\Session::egw_setcookie('domain'); Api\Session::egw_setcookie('domain');

View File

@ -1919,8 +1919,9 @@ body {
background-color: transparent; background-color: transparent;
} }
#loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] { #loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] {
text-indent: 60%; background-image: url(../images/task.png);
background-color: transparent; background-repeat: no-repeat;
background-position-x: 0;
} }
#loginMainDiv div#centerBox form table.divLoginbox select:focus, #loginMainDiv div#centerBox form table.divLoginbox select:focus,
#loginMainDiv div#centerBox form table.divLoginbox select:hover { #loginMainDiv div#centerBox form table.divLoginbox select:hover {
@ -1958,9 +1959,9 @@ body {
background-image: url(../images/password.png); background-image: url(../images/password.png);
} }
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me {
background-image: url(../images/task.png); background-image: none;
z-index: 0; z-index: 0;
width: 130px; width: 230px;
padding-left: 27px; padding-left: 27px;
} }
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.domain { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.domain {
@ -1969,6 +1970,11 @@ body {
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.language { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.language {
background-image: url(../images/language.png); background-image: url(../images/language.png);
} }
#loginMainDiv div#centerBox form table.divLoginbox input[type="checkbox"] {
height: 25px;
margin-top: 7px;
width: auto;
}
#loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] { #loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] {
background-color: #0a5ca5; background-color: #0a5ca5;
color: #ffffff; color: #ffffff;

View File

@ -1908,8 +1908,9 @@ body {
background-color: transparent; background-color: transparent;
} }
#loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] { #loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] {
text-indent: 60%; background-image: url(../images/task.png);
background-color: transparent; background-repeat: no-repeat;
background-position-x: 0;
} }
#loginMainDiv div#centerBox form table.divLoginbox select:focus, #loginMainDiv div#centerBox form table.divLoginbox select:focus,
#loginMainDiv div#centerBox form table.divLoginbox select:hover { #loginMainDiv div#centerBox form table.divLoginbox select:hover {
@ -1947,9 +1948,9 @@ body {
background-image: url(../images/password.png); background-image: url(../images/password.png);
} }
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me {
background-image: url(../images/task.png); background-image: none;
z-index: 0; z-index: 0;
width: 130px; width: 230px;
padding-left: 27px; padding-left: 27px;
} }
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.domain { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.domain {
@ -1958,6 +1959,11 @@ body {
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.language { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.language {
background-image: url(../images/language.png); background-image: url(../images/language.png);
} }
#loginMainDiv div#centerBox form table.divLoginbox input[type="checkbox"] {
height: 25px;
margin-top: 7px;
width: auto;
}
#loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] { #loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] {
background-color: #0a5ca5; background-color: #0a5ca5;
color: #ffffff; color: #ffffff;

View File

@ -1919,8 +1919,9 @@ body {
background-color: transparent; background-color: transparent;
} }
#loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] { #loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] {
text-indent: 60%; background-image: url(../images/task.png);
background-color: transparent; background-repeat: no-repeat;
background-position-x: 0;
} }
#loginMainDiv div#centerBox form table.divLoginbox select:focus, #loginMainDiv div#centerBox form table.divLoginbox select:focus,
#loginMainDiv div#centerBox form table.divLoginbox select:hover { #loginMainDiv div#centerBox form table.divLoginbox select:hover {
@ -1958,9 +1959,9 @@ body {
background-image: url(../images/password.png); background-image: url(../images/password.png);
} }
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me {
background-image: url(../images/task.png); background-image: none;
z-index: 0; z-index: 0;
width: 130px; width: 230px;
padding-left: 27px; padding-left: 27px;
} }
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.domain { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.domain {
@ -1969,6 +1970,11 @@ body {
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.language { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.language {
background-image: url(../images/language.png); background-image: url(../images/language.png);
} }
#loginMainDiv div#centerBox form table.divLoginbox input[type="checkbox"] {
height: 25px;
margin-top: 7px;
width: auto;
}
#loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] { #loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] {
background-color: #0a5ca5; background-color: #0a5ca5;
color: #ffffff; color: #ffffff;

View File

@ -189,8 +189,9 @@
background-color: transparent; background-color: transparent;
} }
select[name="remember_me"] { select[name="remember_me"] {
text-indent: 60%; background-image: url(../images/task.png);
background-color: transparent; background-repeat: no-repeat;
background-position-x: 0;
} }
select:focus, select:hover {box-shadow:none;} select:focus, select:hover {box-shadow:none;}
input { input {
@ -221,9 +222,14 @@
} }
span.field_icons.username {background-image: url(../images/personal.png);} span.field_icons.username {background-image: url(../images/personal.png);}
span.field_icons.password {background-image: url(../images/password.png);} span.field_icons.password {background-image: url(../images/password.png);}
span.field_icons.remember_me {background-image: url(../images/task.png);z-index:0;width: 130px;padding-left: 27px;} span.field_icons.remember_me {background-image: none;z-index:0;width: 230px;padding-left: 27px;}
span.field_icons.domain {background-image: url(../images/internet.png);} span.field_icons.domain {background-image: url(../images/internet.png);}
span.field_icons.language {background-image: url(../images/language.png);} span.field_icons.language {background-image: url(../images/language.png);}
input[type="checkbox"] {
height: 25px;
margin-top: 7px;
width: auto;
}
input[type="submit"] { input[type="submit"] {
background-color: #0a5ca5; background-color: #0a5ca5;
.color_0_gray; .color_0_gray;

View File

@ -51,7 +51,7 @@
<!-- BEGIN remember_me_selection --> <!-- BEGIN remember_me_selection -->
<tr> <tr>
<td> <td>
<span class="field_icons remember_me">{lang_remember_me}:</span> <label for="remember_me" title="{lang_remember_me_help}"><span class="field_icons remember_me" style="background-image: none">{lang_remember_me}</span></label>
{select_remember_me} {select_remember_me}
</td> </td>
</tr> </tr>

View File

@ -1930,8 +1930,9 @@ body {
background-color: transparent; background-color: transparent;
} }
#loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] { #loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] {
text-indent: 60%; background-image: url(../images/task.png);
background-color: transparent; background-repeat: no-repeat;
background-position-x: 0;
} }
#loginMainDiv div#centerBox form table.divLoginbox select:focus, #loginMainDiv div#centerBox form table.divLoginbox select:focus,
#loginMainDiv div#centerBox form table.divLoginbox select:hover { #loginMainDiv div#centerBox form table.divLoginbox select:hover {
@ -1969,9 +1970,9 @@ body {
background-image: url(../images/password.png); background-image: url(../images/password.png);
} }
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me {
background-image: url(../images/task.png); background-image: none;
z-index: 0; z-index: 0;
width: 130px; width: 230px;
padding-left: 27px; padding-left: 27px;
} }
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.domain { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.domain {
@ -1980,6 +1981,11 @@ body {
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.language { #loginMainDiv div#centerBox form table.divLoginbox span.field_icons.language {
background-image: url(../images/language.png); background-image: url(../images/language.png);
} }
#loginMainDiv div#centerBox form table.divLoginbox input[type="checkbox"] {
height: 25px;
margin-top: 7px;
width: auto;
}
#loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] { #loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] {
background-color: #0a5ca5; background-color: #0a5ca5;
color: #ffffff; color: #ffffff;

View File

@ -192,16 +192,6 @@
</td> </td>
</tr> </tr>
<tr class="row_off">
<td>{lang_Allow_authentication_via_cookie}:</td>
<td>
<select name="newsettings[allow_cookie_auth]">
<option value="">{lang_No}</option>
<option value="True" {selected_allow_cookie_auth_True}>{lang_Yes}</option>
</select>
</td>
</tr>
<tr class="row_on"> <tr class="row_on">
<td>{lang_Auto_login_anonymous_user}:</td> <td>{lang_Auto_login_anonymous_user}:</td>
<td> <td>