forked from extern/egroupware
* Login: RememberMe token for either automatic login or as 2. factor for 2-Factor-Auth
This commit is contained in:
parent
e9215fa805
commit
2776d215e2
@ -156,6 +156,9 @@
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row>
|
||||
<description value="2-Factor-Authentication" span="all" class="subHeader"/>
|
||||
</row>
|
||||
<row>
|
||||
<description value="2-Factor-Authentication for interactive login" label="%s:"/>
|
||||
<select id="newsettings[2fa_required]">
|
||||
@ -166,15 +169,57 @@
|
||||
</select>
|
||||
</row>
|
||||
<row>
|
||||
<description value="Cookie path (allows multiple eGW sessions with different directories, has problemes with SiteMgr!)" label="%s:"/>
|
||||
<select id="newsettings[cookiepath]">
|
||||
<option value="">Document root (default)</option>
|
||||
<option value="egroupware">EGroupware directory</option>
|
||||
</select>
|
||||
<vbox>
|
||||
<description value="Allow user to set 'Remember me' token" label="%s:"/>
|
||||
<description value="Requires 'OpenID / OAuth2 Server' app." label="(%s)"/>
|
||||
</vbox>
|
||||
<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>
|
||||
<description value="Cookie domain (default empty means use full domain name, for SiteMgr eg. ".domain.com" allows to use the same cookie for egw.domain.com and www.domain.com)" label="%s:"/>
|
||||
<textbox id="newsettings[cookiedomain]"/>
|
||||
<description value="Lifetime of 'Remember me' token" label="%s:"/>
|
||||
<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>
|
||||
<vbox>
|
||||
@ -195,48 +240,19 @@
|
||||
</select>
|
||||
</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>
|
||||
<description value="Cookie path (allows multiple eGW sessions with different directories, has problemes with SiteMgr!)" label="%s:"/>
|
||||
<select id="newsettings[cookiepath]">
|
||||
<option value="">Document root (default)</option>
|
||||
<option value="egroupware">EGroupware directory</option>
|
||||
</select>
|
||||
</row>
|
||||
<!--
|
||||
<row>
|
||||
<description value="Default file system space per user"/>
|
||||
<textbox id="newsettings[vfs_default_account_size_number]" type="text" size="7"/>
|
||||
<description value="Cookie domain (default empty means use full domain name, for SiteMgr eg. ".domain.com" allows to use the same cookie for egw.domain.com and www.domain.com)" label="%s:"/>
|
||||
<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}">
|
||||
<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>
|
||||
<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"/>
|
||||
<description value="Passwords" span="all" class="subHeader"/>
|
||||
</row>
|
||||
<row>
|
||||
<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>
|
||||
</select>
|
||||
</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}">
|
||||
<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>
|
||||
<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"/>
|
||||
|
@ -418,6 +418,7 @@ distribution lists as groups groupdav de Verteilerlisten als Gruppen
|
||||
djibouti common de DSCHIBUTI
|
||||
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 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 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?
|
||||
@ -1082,7 +1083,8 @@ refresh common de Aktualisierung
|
||||
register common de Registrieren
|
||||
regular common de Normal
|
||||
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 selected accounts common de Ausgewählte Benutzer entfernen
|
||||
remove shortcut common de Abkürzung entfernen
|
||||
|
@ -418,6 +418,7 @@ distribution lists as groups groupdav en Distribution lists as groups
|
||||
djibouti common en DJIBOUTI
|
||||
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 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 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?
|
||||
@ -1083,6 +1084,7 @@ register common en Register
|
||||
regular common en Regular
|
||||
reject common en Reject
|
||||
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 selected accounts common en Remove selected accounts
|
||||
remove shortcut common en Remove shortcut
|
||||
|
@ -223,18 +223,31 @@ class Login
|
||||
* 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_var('lang_remember_me',lang('Remember me'));
|
||||
$tmpl->set_var('select_remember_me',Api\Html::select('remember_me', '', array(
|
||||
'' => lang('not'),
|
||||
'1hour' => lang('1 Hour'),
|
||||
'1day' => lang('1 Day'),
|
||||
'1week'=> lang('1 Week'),
|
||||
'1month' => lang('1 Month'),
|
||||
'forever' => lang('Forever'),
|
||||
),true,'tabindex="3"',0,false));
|
||||
$help = htmlspecialchars(lang('Do NOT use on public computers!'));
|
||||
$tmpl->set_var('lang_remember_me_help', $help);
|
||||
if ($GLOBALS['egw_info']['server']['remember_me_lifetime'] === 'user')
|
||||
{
|
||||
$tmpl->set_var('lang_remember_me', '');
|
||||
$tmpl->set_var('select_remember_me',Api\Html::select('remember_me', '', array(
|
||||
'' => lang('Do not remember me'),
|
||||
'P1W'=> lang('Remember me for %1', lang('1 Week')),
|
||||
'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
|
||||
{
|
||||
|
@ -23,6 +23,8 @@ namespace EGroupware\Api;
|
||||
|
||||
use PragmaRX\Google2FA;
|
||||
use EGroupware\Api\Mail\Credentials;
|
||||
use EGroupware\OpenID;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
|
||||
/**
|
||||
* Create, verifies or destroys an EGroupware session
|
||||
@ -75,6 +77,11 @@ class Session
|
||||
*/
|
||||
const EGW_SESSION_NAME = 'sessionid';
|
||||
|
||||
/**
|
||||
* Name of cookie with remember me token
|
||||
*/
|
||||
const REMEMBER_ME_COOKIE = 'eGW_remember';
|
||||
|
||||
/**
|
||||
* 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 $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 $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
|
||||
*/
|
||||
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 {
|
||||
if (is_array($login))
|
||||
@ -498,6 +506,12 @@ class Session
|
||||
|
||||
$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
|
||||
$GLOBALS['egw_info']['server']['global_denied_users'][$this->account_lid] ||
|
||||
$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));
|
||||
|
||||
// if we have a second factor, check it before forced password change
|
||||
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'))
|
||||
if ($check_2fa !== false)
|
||||
{
|
||||
$google2fa = new Google2FA\Google2FA();
|
||||
try {
|
||||
if (empty($check_2fa) || empty($creds))
|
||||
{
|
||||
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);
|
||||
}
|
||||
$this->checkMultifactorAuth($check_2fa, $_COOKIE[self::REMEMBER_ME_COOKIE]);
|
||||
}
|
||||
catch(\Exception $e) {
|
||||
$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_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");
|
||||
|
||||
// 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(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))).
|
||||
", '$passwd_type', no_session=".array2string($no_session).
|
||||
", 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
|
||||
*
|
||||
|
104
login.php
104
login.php
@ -136,44 +136,27 @@ else
|
||||
// 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']))
|
||||
{
|
||||
$_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;
|
||||
$login = $_SERVER['PHP_AUTH_USER'];
|
||||
$passwd = $_SERVER['PHP_AUTH_PW'];
|
||||
$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
|
||||
# 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:
|
||||
// CN=john.doe/OU=Department/O=Company/C=xx/Email=john@comapy.tld/L=City/
|
||||
@ -203,20 +186,33 @@ else
|
||||
unset($val);
|
||||
unset($sslattributes);
|
||||
}
|
||||
|
||||
if(isset($passwd_type) || $_POST['submitit_x'] || $_POST['submitit_y'] || $submit)
|
||||
else
|
||||
{
|
||||
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']))
|
||||
{
|
||||
$GLOBALS['egw']->session->egw_setcookie('eGW_remember','',0,'/');
|
||||
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
|
||||
if($submit == false)
|
||||
@ -252,7 +248,7 @@ else
|
||||
}
|
||||
}
|
||||
$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)
|
||||
{
|
||||
@ -278,40 +274,10 @@ else
|
||||
}
|
||||
elseif (!isset($GLOBALS['sessionid']) || ! $GLOBALS['sessionid'])
|
||||
{
|
||||
Api\Session::egw_setcookie('eGW_remember','',0,'/');
|
||||
Egw::redirect_link('/login.php?cd=' . $GLOBALS['egw']->session->cd_reason);
|
||||
}
|
||||
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']) &&
|
||||
$_POST['lang'] != $GLOBALS['egw_info']['user']['preferences']['common']['lang'])
|
||||
{
|
||||
|
@ -37,13 +37,18 @@ elseif(strpos($redirectTarget, '[?&]cd=') !== false)
|
||||
$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)
|
||||
{
|
||||
Api\Hooks::process('logout');
|
||||
$GLOBALS['egw']->session->destroy($GLOBALS['sessionid'],$GLOBALS['kp3']);
|
||||
}
|
||||
|
||||
Api\Session::egw_setcookie('eGW_remember','',0,'/');
|
||||
Api\Session::egw_setcookie('sessionid');
|
||||
Api\Session::egw_setcookie('kp3');
|
||||
Api\Session::egw_setcookie('domain');
|
||||
|
@ -1919,8 +1919,9 @@ body {
|
||||
background-color: transparent;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] {
|
||||
text-indent: 60%;
|
||||
background-color: transparent;
|
||||
background-image: url(../images/task.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position-x: 0;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select:focus,
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select:hover {
|
||||
@ -1958,9 +1959,9 @@ body {
|
||||
background-image: url(../images/password.png);
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me {
|
||||
background-image: url(../images/task.png);
|
||||
background-image: none;
|
||||
z-index: 0;
|
||||
width: 130px;
|
||||
width: 230px;
|
||||
padding-left: 27px;
|
||||
}
|
||||
#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 {
|
||||
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"] {
|
||||
background-color: #0a5ca5;
|
||||
color: #ffffff;
|
||||
|
@ -1908,8 +1908,9 @@ body {
|
||||
background-color: transparent;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] {
|
||||
text-indent: 60%;
|
||||
background-color: transparent;
|
||||
background-image: url(../images/task.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position-x: 0;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select:focus,
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select:hover {
|
||||
@ -1947,9 +1948,9 @@ body {
|
||||
background-image: url(../images/password.png);
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me {
|
||||
background-image: url(../images/task.png);
|
||||
background-image: none;
|
||||
z-index: 0;
|
||||
width: 130px;
|
||||
width: 230px;
|
||||
padding-left: 27px;
|
||||
}
|
||||
#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 {
|
||||
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"] {
|
||||
background-color: #0a5ca5;
|
||||
color: #ffffff;
|
||||
|
@ -1919,8 +1919,9 @@ body {
|
||||
background-color: transparent;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] {
|
||||
text-indent: 60%;
|
||||
background-color: transparent;
|
||||
background-image: url(../images/task.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position-x: 0;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select:focus,
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select:hover {
|
||||
@ -1958,9 +1959,9 @@ body {
|
||||
background-image: url(../images/password.png);
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me {
|
||||
background-image: url(../images/task.png);
|
||||
background-image: none;
|
||||
z-index: 0;
|
||||
width: 130px;
|
||||
width: 230px;
|
||||
padding-left: 27px;
|
||||
}
|
||||
#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 {
|
||||
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"] {
|
||||
background-color: #0a5ca5;
|
||||
color: #ffffff;
|
||||
|
@ -189,8 +189,9 @@
|
||||
background-color: transparent;
|
||||
}
|
||||
select[name="remember_me"] {
|
||||
text-indent: 60%;
|
||||
background-color: transparent;
|
||||
background-image: url(../images/task.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position-x: 0;
|
||||
}
|
||||
select:focus, select:hover {box-shadow:none;}
|
||||
input {
|
||||
@ -221,9 +222,14 @@
|
||||
}
|
||||
span.field_icons.username {background-image: url(../images/personal.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.language {background-image: url(../images/language.png);}
|
||||
input[type="checkbox"] {
|
||||
height: 25px;
|
||||
margin-top: 7px;
|
||||
width: auto;
|
||||
}
|
||||
input[type="submit"] {
|
||||
background-color: #0a5ca5;
|
||||
.color_0_gray;
|
||||
|
@ -51,7 +51,7 @@
|
||||
<!-- BEGIN remember_me_selection -->
|
||||
<tr>
|
||||
<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}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1930,8 +1930,9 @@ body {
|
||||
background-color: transparent;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select[name="remember_me"] {
|
||||
text-indent: 60%;
|
||||
background-color: transparent;
|
||||
background-image: url(../images/task.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position-x: 0;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select:focus,
|
||||
#loginMainDiv div#centerBox form table.divLoginbox select:hover {
|
||||
@ -1969,9 +1970,9 @@ body {
|
||||
background-image: url(../images/password.png);
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox span.field_icons.remember_me {
|
||||
background-image: url(../images/task.png);
|
||||
background-image: none;
|
||||
z-index: 0;
|
||||
width: 130px;
|
||||
width: 230px;
|
||||
padding-left: 27px;
|
||||
}
|
||||
#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 {
|
||||
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"] {
|
||||
background-color: #0a5ca5;
|
||||
color: #ffffff;
|
||||
|
@ -192,16 +192,6 @@
|
||||
</td>
|
||||
</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">
|
||||
<td>{lang_Auto_login_anonymous_user}:</td>
|
||||
<td>
|
||||
|
Loading…
Reference in New Issue
Block a user