yf_auth_user classSpecial methods for user authentification.
| File | plugins/auth/classes/auth/yf_auth_user.class.php |
|---|---|
| Use | _class('auth_user', 'classes/auth/') |
A site sets them in its config with $PROJECT_CONF['auth_user']['NAME'] = ...;
| Property | Default | Description |
|---|---|---|
$ALLOW_PLAINTEXT_PASSWORDSbool |
true |
Accept a plaintext stored password at login (yf_register stored them that way); previous behaviour |
$ALLOW_REMEMBER_MEbool |
true |
Allow to use 'remember me in cookies' feature @security |
$AUTH_ONLY_HTTPSbool |
false |
Allow to login only by HTTPS protocol, else raise error @security |
$BLOCK_BANNED_IPSbool |
false |
Track banned IPs list @security |
$BLOCK_FAILED_LOGINSbool |
false |
Block failed logins after several attempts (To prevent password bruteforcing, hacking, etc) @security |
$BLOCK_FAILED_TTLbool |
3600 |
Track failed logins TTL @security |
$BLOCK_FAILS_BY_IP_COUNTbool |
10 |
@security |
$BLOCK_FAILS_BY_LOGIN_COUNTbool |
5 |
@security |
$CATCH_REF_CODESbool |
false |
Catch ref codes |
$CHECK_MULTI_ACCOUNTSbool |
false |
Check if user have multiple accounts @security |
$COOKIE_DOMAINstring |
'' |
|
$COOKIE_HTTPONLYstring |
true |
|
$COOKIE_PATHstring |
'/' |
|
$COOKIE_SECUREstring |
false |
|
$DEF_USER_MODULEstring |
'signup' |
Default user object (module) to redirect |
$DO_LOG_LOGINSbool |
true |
Do log into db user login actions |
$EXEC_AFTER_INITarray |
[] |
@conf_skip |
$EXEC_AFTER_LOGINarray |
array(array('test_method', array('Working!'))); */
Property [ public $EXEC_AFTER_LOGIN = [] |
@conf_skip Methods to execute after success login or logout @example $EXEC_AFTER_LOGIN = array(array('test_method', array('Working!'))); |
$EXEC_AFTER_LOGOUTarray |
[] |
@conf_skip |
$FORCE_LOGOUT_INACTIVEstring |
true |
|
$GEO_COOKIE_JSONbool |
false |
SECURITY: the geo_selected cookie is written as JSON and read as JSON (an old serialize()d array is still read, without objects); default false keeps serialize() / unserialize() (4.0 flips) |
$INSIDE_LOGIN_FROM_ADMIN |
false |
|
$LOGIN_ALIASstring |
['email', 'phone'] |
Alternative login field name to use @conf_skip |
$LOGIN_FIELDstring |
'login' |
Login field name to use @conf_skip |
$LOG_FAILED_LOGINSbool |
true |
Save failed logins @security |
$LOG_FAILED_PSWDbool |
true |
Store the submitted password in log_auth_fails (previous behaviour); false keeps secrets out of the log |
$ONLINE_AUTO_CLEANint |
1800 |
Online TTL if inactive |
$ONLINE_MAX_IDSint |
1000 |
Online TTL if inactive |
$ONLINE_SKIP_PAGESarray |
['user_profile->compact_info', 'help->show_tip', 'forum->compact_topic_repliers', 'aff', 'task_loader'] |
Pages where we do not need to track online stats @conf_skip |
$PSWD_FIELDstring |
'password' |
Password field name to use @conf_skip |
$REMEMBER_COOKIE_PREFIXstring |
'r1.' |
Cookie format marker of the token cookie |
$REMEMBER_FIELDstring |
'remember_me' |
Remeber field name to use @conf_skip |
$REMEMBER_ME_TOKENSbool |
false |
Remember-me as a random server-side token (user_remember_tokens, hashed, rotated on use) instead of the encrypted login+password cookie; old cookies expire once enabled |
$REMEMBER_ROTATE_GRACEint |
30 |
Seconds the previous validator stays valid after a rotation, for parallel requests that still carry it |
$SESSION_LOCK_TO_HOSTbool |
false |
Lock session to hostname string (to prevent hacks) @security |
$SESSION_LOCK_TO_IPbool |
true |
Lock session to IP address (to prevent hacks) @security |
$SESSION_LOCK_TO_UAbool |
false |
Lock session to User Agent string (to prevent hacks) @security |
$SESSION_REFERER_CHECKbool |
false |
Check referer in session @security |
$SET_IS_LOGGED_COOKIEbool |
'' |
Set cookie 'is_logged_in', different from member_id, useful for nginx cache difference, if field empty - will be disabled |
$SET_MEMBER_ID_COOKIEbool |
'' |
Set cookie 'member_id', useful for fast_init before session start |
$SITE_CLOSED_STPLstring |
'site_closed' |
Site closed stpl name |
$STORE_ONLINE_USERSbool |
false |
Online users vars |
$TRACK_GEO_LOCATIONbool |
false |
Store cookie with geo info for guests |
$URL_ACCOUNT_INACTIVEstring |
'./?object=login_form&action=account_inactive' |
Redirect URL |
$URL_AFTER_LOGOUTstring |
'./' |
Redirect URL |
$URL_SUCCESS_LOGINstring |
'' |
Redirect URL |
$URL_WRONG_LOGINstring |
'./?object=login_form&action=wrong_login' |
Redirect URL |
$USER_INFO_IN_SESSIONbool |
0 |
Store or not user info in session sub array or not |
$USER_PASSWORD_SALTstring |
'' |
|
$USER_SECURITY_CHECKSstring |
false |
|
$VAR_COOKIE_LIFE_TIMEint |
2592000 |
Cookie TTL |
$VAR_COOKIE_NAMEstring |
'remember_user_id' |
Cookie name @conf_skip |
$VAR_LOCK_HOSTstring |
'auth_lock_to_host' |
field name @conf_skip |
$VAR_LOCK_IPstring |
'auth_lock_to_ip' |
field name @conf_skip |
$VAR_LOCK_UAstring |
'auth_lock_to_ua' |
field name @conf_skip |
$VAR_USER_GO_URLstring |
'user_go_url' |
field name @conf_skip |
$VAR_USER_GROUP_IDstring |
'user_group' |
field name @conf_skip |
$VAR_USER_IDstring |
'user_id' |
field name @conf_skip |
$VAR_USER_INFOstring |
'user_info' |
field name @conf_skip |
$VAR_USER_LOGIN_TIMEstring |
'user_login_time' |
field name @conf_skip |
$_encrypted_error |
null |
public function auto_login($user_info = [], $no_redirect_on_success = false)
Alias.
public function auto_login($user_info = [], $no_redirect_on_success = false)
{
return $this->_save_login_in_session($user_info, $no_redirect_on_success);
}
public function init()
Initialize auth.
public function init()
{
// Chained config rule
if ($this->BLOCK_FAILED_LOGINS) {
$this->LOG_FAILED_LOGINS = true;
}
// Check if we need to skip online stats
if ($this->STORE_ONLINE_USERS) {
if (in_array(($_GET['object'] ?? null) . '->' . ($_GET['action'] ?? null), $this->ONLINE_SKIP_PAGES) || in_array(($_GET['object'] ?? null), $this->ONLINE_SKIP_PAGES)) {
$this->STORE_ONLINE_USERS = false;
}
// Check CPU Load
if (conf('HIGH_CPU_LOAD') == 1) {
$this->STORE_ONLINE_USERS = false;
}
}
// Delete expired users (expiration time for now 300 seconds == 5 minutes)
// make this to run randomly every ~10th page call
// TODO: this needed to be done on global cron rarely (every minute or every hour), not on every request
if ($this->STORE_ONLINE_USERS && $this->ONLINE_AUTO_CLEAN && rand(1, 10) == 1) {
db()->_add_shutdown_query('DELETE FROM ' . db('online') . ' WHERE (' . time() . '-time) > ' . (int) ($this->ONLINE_AUTO_CLEAN));
}
// Remember last query string to process it after succesful login
if (empty($_SESSION[$this->VAR_USER_ID]) && isset($_GET['task']) && ($_GET['task'] ?? null) == 'login') {
if (false === strpos(($_SERVER['HTTP_REFERER'] ?? ''), 'login')) {
$_SESSION[$this->VAR_USER_GO_URL] = ($_SERVER['HTTP_REFERER'] ?? null);
}
}
// Check for session IP
if ($this->SESSION_LOCK_TO_IP && ! empty($_SESSION[$this->VAR_USER_ID]) && ($_GET['task'] ?? null) !== 'logout') {
// User has changed IP, logout immediately
$ip = common()->get_ip();
if ( ! isset($_SESSION[$this->VAR_LOCK_IP]) || ($_SESSION[$this->VAR_LOCK_IP] ?? null) !== $ip) {
trigger_error('AUTH: Attempt to use session with changed IP blocked, auth_ip:' . ($_SESSION[$this->VAR_LOCK_IP] ?? null) . ', new_ip:' . $ip . ', user_id: ' . (int) ($_SESSION[$this->VAR_USER_ID] ?? null), E_USER_WARNING);
$this->_log_fail(['reason' => 'auth_blocked_by_ip']);
$_GET['task'] = 'logout';
}
}
// Check for session User Agent
if ($this->SESSION_LOCK_TO_UA && ! empty($_SESSION[$this->VAR_USER_ID]) && ($_GET['task'] ?? null) !== 'logout') {
// User has changed User Agent, logout immediately
$ua = ($_SERVER['HTTP_USER_AGENT'] ?? null);
if ( ! isset($_SESSION[$this->VAR_LOCK_UA]) || ($_SESSION[$this->VAR_LOCK_UA] ?? null) !== $ua) {
trigger_error('AUTH: Attempt to use session with changed User Agent blocked, auth_ua:"' . ($_SESSION[$this->VAR_LOCK_UA] ?? null) . '", new_ua:"' . $ua . '", user_id: ' . (int) ($_SESSION[$this->VAR_USER_ID] ?? null), E_USER_WARNING);
$this->_log_fail(['reason' => 'auth_blocked_by_ua']);
$_GET['task'] = 'logout';
}
}
// Check for session hostname
if ($this->SESSION_LOCK_TO_HOST && ! empty($_SESSION[$this->VAR_USER_ID]) && ($_GET['task'] ?? null) !== 'logout') {
// User has changed Host, logout immediately
$host = $_SERVER['HTTP_HOST'];
if ( ! isset($_SESSION[$this->VAR_LOCK_HOST]) || $_SESSION[$this->VAR_LOCK_HOST] !== $host) {
trigger_error('AUTH: Attempt to use session with changed Host blocked, auth_host:"' . $_SESSION[$this->VAR_LOCK_HOST] . '", new_host:"' . $host . '", user_id: ' . (int) ($_SESSION[$this->VAR_USER_ID]), E_USER_WARNING);
$this->_log_fail(['reason' => 'auth_blocked_by_host']);
$_GET['task'] = 'logout';
}
}
// Check referer matched to WEB_PATH
$referer = ($_SERVER['HTTP_REFERER'] ?? null);
if ($this->SESSION_REFERER_CHECK && ( ! $referer || substr($referer, 0, strlen(WEB_PATH)) != WEB_PATH) && ($_GET['task'] ?? null) !== 'logout') {
trigger_error('AUTH: Referer not matched and session blocked, referer:' . $referer, E_USER_WARNING);
$this->_log_fail(['reason' => 'auth_blocked_by_referer']);
$_GET['task'] = 'logout';
}
// This code needed to auto-logout previously logged-in user, but disabled later
if ($this->FORCE_LOGOUT_INACTIVE && ($_SESSION[$this->VAR_USER_ID] ?? null) && ($_GET['task'] ?? null) !== 'logout') {
$main = main();
$main->_init_cur_user_info($main);
$user = &$main->_user_info;
if (isset($user['id']) && ! $user['active']) {
trigger_error('AUTH: Force logout inactive account', E_USER_WARNING);
$this->_log_fail(['reason' => 'auth_logout_inactive']);
$_GET['task'] = 'logout';
}
}
// Switch between login/logout actions
if (isset($_GET['task']) && ($_GET['task'] ?? null) === 'logout') {
return $this->_do_logout();
}
if ( ! empty($_COOKIE[$this->VAR_COOKIE_NAME]) && empty($_SESSION[$this->VAR_USER_ID])) {
$this->_process_cookie();
}
if (isset($_GET['task']) && ($_GET['task'] ?? null) == 'login') {
if (($_GET['id'] ?? null) && strlen(($_GET['id'] ?? '')) > 16 && ! is_numeric($_GET['id'] ?? null)) {
$this->_do_login_with_encrypted();
}
if (empty($_SESSION[$this->VAR_USER_ID])) {
$this->_do_login([
'login' => $_POST[$this->LOGIN_FIELD] ?? '',
'pswd' => $_POST[$this->PSWD_FIELD] ?? '',
]);
}
}
// Check if current user session has expired
if ($this->STORE_ONLINE_USERS) {
$online_users = &main()->_online_users;
if ( ! isset($online_users)) {
$online_users = [];
$sql = 'SELECT id, user_id FROM ' . db('online') . (MAIN_TYPE_USER ? ' WHERE type != "admin"' : '') . ($this->ONLINE_MAX_IDS ? ' LIMIT ' . (int) ($this->ONLINE_MAX_IDS) : '');
foreach ((array) db()->get_2d($sql) as $online_id => $user_id) {
$online_users[$online_id] = $user_id;
}
}
// Create default record if not exists one
if ( ! isset($online_users[session_id()])) {
$_cur_user_data = $this->_update_online_info();
$online_users[session_id()] = $_cur_user_data['user_id'];
}
}
if ($this->TRACK_GEO_LOCATION && main()->USE_GEO_IP) {
$this->_track_geo_location();
}
if ($this->CATCH_REF_CODES) {
$this->_catch_ref_codes();
}
// Try to assign first page of the site (if $_GET['object'] is empty)
if (empty($_GET['object'])) {
$go = defined('SITE_DEFAULT_PAGE') ? SITE_DEFAULT_PAGE : conf('site_first_page');
// Check if default url is not empty and then use it
if ( ! empty($go)) {
$go = str_replace(['./?', './'], '', $go);
$tmp_array = [];
parse_str($go, $tmp_array);
foreach ((array) $tmp_array as $k => $v) {
$_GET[$k] = $v;
}
}
}
if (empty($_GET['object'])) {
$_GET['object'] = $this->DEF_USER_MODULE;
}
// Store user inside the main module
if ( ! empty($_SESSION[$this->VAR_USER_ID]) && ! empty($_SESSION[$this->VAR_USER_GROUP_ID])) {
main()->USER_ID = ($_SESSION[$this->VAR_USER_ID] ?? null);
main()->USER_GROUP = ($_SESSION[$this->VAR_USER_GROUP_ID] ?? null);
if ($this->USER_INFO_IN_SESSION && ! empty($_SESSION[$this->VAR_USER_INFO])) {
main()->USER_INFO = ($_SESSION[$this->VAR_USER_INFO] ?? null);
}
}
$this->_exec_method_on_action('init');
}
public function login_as($id)
public function login_as($id)
{
if ( ! $id) {
return _e('Wrong id');
}
$a = db()->from('user')->whereid($id)->get();
if ( ! $a) {
return _e('Target user not found');
}
$t = time();
$secret_key = db()->get_one('SELECT MD5(CONCAT(`password`, "' . str_replace(['http://', 'https://'], '//', INCLUDE_PATH) . '")) FROM ' . db('admin') . ' WHERE id=1');
$to_encode = 'userid-' . $a['id'] . '-' . $t . '-' . md5($a['password']);
$integrity_hash = md5($to_encode);
$encrypted = _class('encryption')->_safe_encrypt_with_base64($to_encode . '-' . $integrity_hash, $secret_key);
if (tpl()->REWRITE_MODE) {
$url = url_user(['task' => 'login', 'id' => $encrypted], parse_url(WEB_PATH, PHP_URL_HOST));
} else {
$url = WEB_PATH . '?task=login&id=' . $encrypted;
}
return js_redirect($url, $rewrite = false);
}
_ hooks and helpers; not routable as page actionspublic function __construct()
public function __construct()
{
$cookie_life_time = conf('cookie_life_time');
if (isset($cookie_life_time)) {
$this->VAR_COOKIE_LIFE_TIME = 86400 * $cookie_life_time;
}
$online_auto_clean = conf('online_auto_clean');
if (isset($online_auto_clean)) {
$this->ONLINE_AUTO_CLEAN = $online_auto_clean;
}
if (defined('DEFAULT_USER_MODULE')) {
$this->DEF_USER_MODULE = DEFAULT_USER_MODULE;
}
if (empty($this->COOKIE_PATH)) {
$url_parts = @parse_url(WEB_PATH);
$this->COOKIE_PATH = $url_parts['path'];
}
}
public function _catch_ref_codes()
Ref codes.
public function _catch_ref_codes()
{
if ( ! $this->CATCH_REF_CODES) {
return false;
}
$COOKIE_VAR_NAME = 'ref_code';
$where = ($_SERVER['REQUEST_URI'] ?? null);
$ref_code = '';
if ( ! empty($where) && preg_match('/\?r=([0-9]{1,8})$/ims', $where, $m)) {
$ref_code = $m[1];
}
if (empty($ref_code)) {
return false;
}
$this->_cookie_set($COOKIE_VAR_NAME, $ref_code, time() + 86400 * 30);
// Return user to the initial page without ref_code
$location = ((($_SERVER['HTTPS'] ?? null) || ($_SERVER['SSL_PROTOCOL'] ?? null)) ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? null) . substr($where, 0, -strlen('?r=' . $ref_code));
common()->redirect($location, false, 'http', '', 0);
exit();
}
public function _check_multi_accounts()
Multiple accounts checker.
public function _check_multi_accounts()
{
$last_id = null;
if (empty($_SESSION[$this->VAR_USER_ID]) || empty($_SESSION[$this->VAR_USER_GROUP_ID])) {
return false;
}
$_SPECIAL_NAME = 'accounts';
if (empty($_COOKIE[$_SPECIAL_NAME])) {
return $this->_set_special_cookie($_SESSION[$this->VAR_USER_ID] ?? null);
}
$last_id = null;
$cookie_users = [];
foreach (explode('_', ($_COOKIE[$_SPECIAL_NAME] ?? '')) as $_user_id) {
$cookie_users[$_user_id] = $_user_id;
}
if (($_COOKIE[$_SPECIAL_NAME] ?? null) == ($_SESSION[$this->VAR_USER_ID] ?? null)
|| (count((array) $cookie_users) > 1 && isset($cookie_users[$_SESSION[$this->VAR_USER_ID]]))
) {
return false;
}
$data = db()->query_fetch('SELECT * FROM ' . db('check_multi_accounts') . ' WHERE user_id=' . (int) ($_SESSION[$this->VAR_USER_ID] ?? null));
if (isset($cookie_users[$_SESSION[$this->VAR_USER_ID]])) {
unset($cookie_users[$_SESSION[$this->VAR_USER_ID]]);
}
$matching_users = implode(',', $cookie_users);
if (empty($data)) {
db()->insert_safe('check_multi_accounts', [
'user_id' => (int) ($_SESSION[$this->VAR_USER_ID] ?? null),
'matching_users' => $matching_users,
'last_update' => time(),
'cookie_match' => 1,
]);
} else {
db()->update_safe('check_multi_accounts', [
'matching_users' => $matching_users,
'last_update' => time(),
'cookie_match' => 1,
], 'user_id=' . (int) ($_SESSION[$this->VAR_USER_ID] ?? null));
}
$this->_set_special_cookie(($_COOKIE[$_SPECIAL_NAME] ?? null) . '_' . ($_SESSION[$this->VAR_USER_ID] ?? null));
main()->_HAS_MULTI_ACCOUNTS = true;
_debug_log('_check_multi_accounts: found possible multi-account, old_id=' . $last_id . ',new_id=' . ($_SESSION[$this->VAR_USER_ID] ?? null) . ',cookie=' . ($_COOKIE[$_SPECIAL_NAME] ?? null), E_NOTICE);
}
public function _check_password($plain, $stored)
The stored form is detected by yf_password (md5/sha1/sha256 digests of pass . salt, password_hash() strings); the legacy md5(pass . USER_PASSWORD_SALT) and, while ALLOW_PLAINTEXT_PASSWORDS, the plain value stay accepted.
public function _check_password($plain, $stored)
{
return _class('password')->verify($plain, $stored, [
'salts' => [$this->USER_PASSWORD_SALT],
'plain' => $this->ALLOW_PLAINTEXT_PASSWORDS,
]);
}
public function _cleanup_cookie()
Unset user cookie.
public function _cookie_del($name, $value = '', $expire = 0, $path = null, $domain = null, $secure = null, $httponly = null)
public function _cookie_set($name, $value = '', $expire = 0, $path = null, $domain = null, $secure = null, $httponly = null)
public function _do_login($params = [])
Try to log in user.
public function _do_login($params = [])
{
$AUTH_LOGIN = trim($params['login']);
$AUTH_PSWD = trim($params['pswd']);
$cur_ip = common()->get_ip();
if ($this->AUTH_ONLY_HTTPS && ! (($_SERVER['HTTPS'] ?? null) || ($_SERVER['SSL_PROTOCOL'] ?? null))) {
$redirect_url = '';
if ($_SERVER['HTTP_REFERER'] ?? null) {
$redirect_url = str_replace('http://', 'https://', $_SERVER['HTTP_REFERER']);
}
if ( ! $redirect_url) {
$request_uri = getenv('REQUEST_URI');
$cur_web_path = $request_uri[strlen($request_uri) - 1] == '/' ? substr($request_uri, 0, -1) : dirname($request_uri);
$redirect_url = 'https://' . getenv('HTTP_HOST') . str_replace(['\\', '//'], ['/', '/'], (MAIN_TYPE_ADMIN ? dirname($cur_web_path) : $cur_web_path) . '/');
}
if ($params['no_redirect']) {
return false;
}
return js_redirect($redirect_url);
}
$fail_reason = false;
$user_info = null;
if ( ! empty($AUTH_LOGIN) && ! empty($AUTH_PSWD)) {
$NEED_QUERY_DB = true;
if ($this->BLOCK_BANNED_IPS && common()->_ip_is_banned()) {
$fail_reason = 'ip_blocked';
$this->_log_fail([
'login' => $AUTH_LOGIN,
'pswd' => $AUTH_PSWD,
'reason' => $fail_reason,
]);
$msg = 'Attempt to login from banned IP (' . _prepare_html($cur_ip) . ') as "' . _prepare_html($AUTH_LOGIN) . '" blocked';
common()->message_error($msg);
trigger_error('AUTH: ' . $msg, E_USER_WARNING);
return js_redirect($this->URL_WRONG_LOGIN);
}
if ($this->BLOCK_FAILED_LOGINS) {
$min_time = time() - $this->BLOCK_FAILED_TTL;
$fails_by_login = (int) db()->get_one('SELECT COUNT(*) FROM ' . db('log_auth_fails') . ' WHERE time > ' . $min_time . ' AND login="' . _es($AUTH_LOGIN) . '"');
if ($fails_by_login >= $this->BLOCK_FAILS_BY_LOGIN_COUNT) {
$fail_reason = 'blocked_fails_by_login';
$msg = 'Attempt to login as "' . _prepare_html($AUTH_LOGIN) . '" blocked';
common()->message_error($msg);
trigger_error('AUTH: ' . $msg . ', fails_by_login: ' . $fails_by_login, E_USER_WARNING);
} else {
$fails_by_ip = (int) db()->get_one('SELECT COUNT(*) FROM ' . db('log_auth_fails') . ' WHERE time > ' . $min_time . ' AND ip="' . _es($cur_ip) . '"');
if ($fails_by_ip >= $this->BLOCK_FAILS_BY_IP_COUNT) {
$fail_reason = 'blocked_fails_by_ip';
$msg = 'Attempt to login as "' . _prepare_html($AUTH_LOGIN) . '" blocked';
common()->message_error($msg);
trigger_error('AUTH: ' . $msg . ', fails_by_ip: ' . $fails_by_ip, E_USER_WARNING);
}
}
}
if ( ! $fail_reason) {
$PSWD_OK = false;
$user_info = $this->_get_user_info($AUTH_LOGIN);
$stored = is_array($user_info) ? (string) ($user_info['password'] ?? '') : '';
if ( ! empty($params['stored_match'])) {
// the remember-me cookie carries the stored value itself (REMEMBER_ME_TOKENS replaces it with a server-side token)
$PSWD_OK = $stored !== '' && hash_equals($stored, $AUTH_PSWD);
} else {
$check = $this->_check_password($AUTH_PSWD, $stored);
$PSWD_OK = $check['ok'];
if ($PSWD_OK && $check['rehash'] && ! empty($user_info['id'])) {
$new_hash = _class('password')->rehash('user', $user_info['id'], $stored, $AUTH_PSWD);
if ($new_hash) {
// the remember-me cookie built below must carry the value now stored
$user_info['password'] = $new_hash;
}
}
}
if ( ! $PSWD_OK) {
$fail_reason = 'wrong_login';
}
}
if ( ! $fail_reason && $this->USER_SECURITY_CHECKS) {
$fail_reason = $this->_user_security_checks($user_info['id']);
}
if ($fail_reason) {
$user_info = null;
$this->_log_fail([
'login' => $AUTH_LOGIN,
'pswd' => $AUTH_PSWD,
'reason' => $fail_reason,
]);
}
}
return $this->_save_login_in_session($user_info, $params['no_redirect']);
}
public function _do_login_with_encrypted()
Try to log in user with encrypted string (used to quickly login admin as common user).
public function _do_login_with_encrypted()
{
header('X-Robots-Tag: noindex,nofollow,noarchive,nosnippet');
if ( ! ($_GET['id'] ?? null) || strlen(($_GET['id'] ?? '')) < 16 || is_numeric($_GET['id'] ?? null)) {
$this->_encrypted_error = 'GET_id is not like an encrypted string';
return false;
}
$secret_key = db()->get_one('SELECT MD5(CONCAT(`password`, "' . str_replace(['http://', 'https://'], '//', INCLUDE_PATH) . '")) FROM ' . db('admin') . ' WHERE id=1');
if ( ! $secret_key) {
$this->_encrypted_error = 'secret key generation failed';
return false;
}
// Should contain this: 'userid-%id-%time-%md5';
$decrypted = _class('encryption')->_safe_decrypt_with_base64(($_GET['id'] ?? null), $secret_key);
if ( ! $decrypted || ! strlen($decrypted) || substr($decrypted, 0, strlen('userid-')) != 'userid-') {
$this->_encrypted_error = 'decryption failed, possibly broken string or hack attempt';
return false;
}
$d = explode('-', $decrypted);
if (count((array) $d) != 5) {
$this->_encrypted_error = 'decrypted string is not a valid array';
return false;
}
if (md5($d[0] . '-' . $d[1] . '-' . $d[2] . '-' . $d[3]) != $d[4]) {
$this->_encrypted_error = 'md5 hash not matches';
return false;
}
$user_id = (int) ($d[1]);
$time = (int) ($d[2]);
$pswd_hash = strtolower(trim($d[3]));
if ( ! $user_id || ! $time) {
$this->_encrypted_error = 'wrong user_id or time';
return false;
}
if ( ! preg_match('~^[a-z0-9]{32}$~', $pswd_hash)) {
$this->_encrypted_error = 'wrong pswd_hash';
return false;
}
// Allowing only 6 hours for link to keep alive
if ($time < (time() - 3600 * 6)) {
$this->_encrypted_error = 'time elapsed';
return false;
}
$user_info = db()->get('SELECT *, MD5(`password`) AS md5_pswd FROM ' . db('user') . ' WHERE id=' . (int) $user_id);
if ( ! $user_info || $user_info['id'] != $user_id) {
$this->_encrypted_error = 'user not found';
return false;
}
if ( ! $user_info['md5_pswd'] || $user_info['md5_pswd'] != $pswd_hash) {
$this->_encrypted_error = 'user pswd_hash not matched';
return false;
}
$this->INSIDE_LOGIN_FROM_ADMIN = true;
return $this->_save_login_in_session($user_info);
}
public function _do_logout($no_redirect = false)
Do log out user.
public function _do_logout($no_redirect = false)
{
if ($this->STORE_ONLINE_USERS) {
db()->_add_shutdown_query('DELETE FROM ' . db('online') . ' WHERE user_id=' . (int) ($_SESSION[$this->VAR_USER_ID] ?? null));
$MAIN = &main();
if (isset($MAIN->_online_users[session_id()])) {
unset($MAIN->_online_users[session_id()]);
}
}
$this->_exec_method_on_action('logout');
if ($this->REMEMBER_ME_TOKENS) {
$this->_remember_revoke(is_string($_COOKIE[$this->VAR_COOKIE_NAME] ?? null) ? $_COOKIE[$this->VAR_COOKIE_NAME] : '');
}
$user_session_vars = [
$this->VAR_USER_ID,
$this->VAR_USER_GROUP_ID,
$this->VAR_USER_LOGIN_TIME,
$this->VAR_LOCK_IP,
$this->VAR_LOCK_UA,
$this->VAR_LOCK_HOST,
];
foreach ((array) $_SESSION as $k => $v) {
if (in_array($k, $user_session_vars)) {
unset($_SESSION[$k]);
}
}
$main = main();
$main->_init_cur_user_info($main);
$main->USER_INFO = &$main->_user_info;
$main->_LOGGED_IN_USER_INFO = &$main->_user_info;
$this->_cleanup_cookie();
session_regenerate_id($destroy = true);
if ($no_redirect) {
return false;
}
js_redirect($this->URL_AFTER_LOGOUT);
}
public function _exec_method_on_action($action = 'login')
Execute user method after specified action.
public function _exec_method_on_action($action = 'login')
{
if ($action == 'login') {
$callbacks = $this->EXEC_AFTER_LOGIN;
} elseif ($action == 'logout') {
$callbacks = $this->EXEC_AFTER_LOGOUT;
} elseif ($action == 'init') {
$callbacks = $this->EXEC_AFTER_INIT;
}
if (empty($callbacks) || ! is_array($callbacks)) {
return false;
}
foreach ((array) $callbacks as $callback) {
if (is_callable($callback)) {
call_user_func_array($callback, ['self' => $this]);
}
}
}
public function _geo_cookie_decode($value)
JSON, or an array serialize()d before GEO_COOKIE_JSON was turned on; anything else is null.
public function _geo_cookie_encode($data)
geo_selected cookie value (written by geo_content::change_location()).
$data arraypublic function _get_user_info($login = '')
public function _get_user_info($login = '')
{
if (empty($login)) {
return false;
}
$sql = 'SELECT * FROM ' . db('user') . ' WHERE ' . db()->escape_key($this->LOGIN_FIELD) . '="' . db()->es($login) . '" ';
$login_aliases = $this->LOGIN_ALIAS;
if ($login_aliases) {
if ( ! is_array($login_aliases)) {
$login_aliases = [$login_aliases];
}
$alias_sql = [];
foreach ((array) $login_aliases as $alias) {
$alias = trim($alias);
if (preg_match('/^[a-z0-9_]+$/ims', $alias)) {
$alias_sql[] = db()->escape_key($alias) . '="' . db()->es($login) . '"';
}
}
if ($alias_sql) {
$sql .= ' OR ' . implode(' OR ', $alias_sql);
}
}
return db()->get($sql);
}
public function _is_user_inactive($user_info = [])
public function _is_user_inactive($user_info = [])
{
if (empty($user_info)) {
return true;
}
if (empty($user_info['active'])) {
return true;
}
return false;
}
public function _log_fail($data = [])
public function _log_fail($data = [])
{
if ( ! $this->LOG_FAILED_LOGINS) {
return false;
}
return db()->insert_safe('log_auth_fails', [
'time' => str_replace(',', '.', microtime(true)),
'ip' => common()->get_ip(),
'user_id' => ($data['user_id'] ?? null) ?: ($_SESSION[$this->VAR_USER_ID] ?? null),
'login' => $data['login'],
'pswd' => $this->LOG_FAILED_PSWD ? $data['pswd'] : '',
'reason' => $data['reason'],
'host' => ($_SERVER['HTTP_HOST'] ?? null),
'user_agent' => ($_SERVER['HTTP_USER_AGENT'] ?? null),
'referer' => ($_SERVER['HTTP_REFERER'] ?? null),
'query_string' => ($_SERVER['QUERY_STRING'] ?? null),
'site_id' => (int) conf('SITE_ID'),
'server_id' => (int) conf('SERVER_ID'),
]);
}
public function _process_cookie()
Processing user cookie.
public function _remember_consume($cookie)
Log the user in from a valid token cookie and rotate the validator; false (and the cookie dropped by the caller) otherwise.
public function _remember_consume($cookie)
{
$parts = $this->_remember_parts($cookie);
if ( ! $parts) {
return false;
}
list($selector, $validator) = $parts;
$row = db()->get('SELECT * FROM ' . db('user_remember_tokens') . ' WHERE selector = "' . _es($selector) . '"', $use_cache = false);
if (empty($row['id'])) {
return false;
}
if ((int) $row['expires'] < time()) {
db()->query('DELETE FROM ' . db('user_remember_tokens') . ' WHERE id = ' . (int) $row['id']);
return false;
}
$hash = $this->_remember_hash($validator);
$current = hash_equals((string) $row['token_hash'], $hash);
// a parallel request (second tab) may still carry the validator rotated a moment ago
$recent = (string) $row['prev_token_hash'] !== '' && hash_equals((string) $row['prev_token_hash'], $hash)
&& (int) $row['rotated'] >= time() - $this->REMEMBER_ROTATE_GRACE;
if ( ! $current && ! $recent) {
// neither the current nor the just-rotated validator: the cookie was copied, drop the row
db()->query('DELETE FROM ' . db('user_remember_tokens') . ' WHERE id = ' . (int) $row['id']);
return false;
}
$user_info = db()->get('SELECT * FROM ' . db('user') . ' WHERE id = ' . (int) $row['user_id'], $use_cache = false);
if (empty($user_info['id'])) {
return false;
}
if ($current) {
// rotate: same selector, new validator, fresh expiry; the old one stays valid for the grace window
$new_validator = bin2hex(random_bytes(32));
$expires = time() + $this->VAR_COOKIE_LIFE_TIME;
db()->update('user_remember_tokens', db()->es_for_builder([
'token_hash' => $this->_remember_hash($new_validator),
'prev_token_hash' => $row['token_hash'],
'rotated' => time(),
'expires' => $expires,
]), (int) $row['id']);
$this->_cookie_set($this->VAR_COOKIE_NAME, $this->_remember_cookie_value($selector, $new_validator), $expires);
}
$_SESSION[$this->VAR_USER_GO_URL] = null;
$this->_save_login_in_session($user_info, $no_redirect_on_success = true);
return true;
}
public function _remember_cookie_value($selector, $validator)
public function _remember_hash($validator)
public function _remember_hash($validator)
{
return hash('sha256', (string) $validator);
}
public function _remember_issue($user_id, $selector = null)
Cookie value 'r1.<selector>.<validator>': the selector finds the row, only sha256(validator) is stored.
public function _remember_issue($user_id, $selector = null)
{
$user_id = (int) $user_id;
if ( ! $user_id) {
return false;
}
$selector = $selector ?: bin2hex(random_bytes(12));
$validator = bin2hex(random_bytes(32));
$expires = time() + $this->VAR_COOKIE_LIFE_TIME;
// abandoned tokens of everybody go when a new one is issued
db()->query('DELETE FROM ' . db('user_remember_tokens') . ' WHERE expires < ' . time());
$row = [
'user_id' => $user_id,
'selector' => $selector,
'token_hash' => $this->_remember_hash($validator),
'prev_token_hash' => '',
'expires' => $expires,
'created' => time(),
'rotated' => time(),
];
$ok = db()->insert('user_remember_tokens', db()->es_for_builder($row));
if ( ! $ok) {
return false;
}
$this->_cookie_set($this->VAR_COOKIE_NAME, $this->_remember_cookie_value($selector, $validator), $expires);
return $selector;
}
public function _remember_parts($cookie)
[selector, validator] from the cookie value, or false when it is not a token cookie.
public function _remember_parts($cookie)
{
if ( ! is_string($cookie)) {
return false;
}
if (strpos($cookie, $this->REMEMBER_COOKIE_PREFIX) !== 0) {
return false;
}
$parts = explode('.', substr($cookie, strlen($this->REMEMBER_COOKIE_PREFIX)));
if (count($parts) !== 2 || ! preg_match('~^[0-9a-f]{24}$~', $parts[0]) || ! preg_match('~^[0-9a-f]{64}$~', $parts[1])) {
return false;
}
return $parts;
}
public function _remember_revoke($cookie)
public function _remember_revoke($cookie)
{
$parts = $this->_remember_parts($cookie);
if ( ! $parts) {
return false;
}
return (bool) db()->query('DELETE FROM ' . db('user_remember_tokens') . ' WHERE selector = "' . _es($parts[0]) . '"');
}
public function _remember_revoke_user($user_id)
"Log out everywhere": after a password change or reset every remembered browser of the user has to log in again.
public function _remember_revoke_user($user_id)
{
$user_id = (int) $user_id;
if ( ! $user_id) {
return false;
}
return (bool) db()->query('DELETE FROM ' . db('user_remember_tokens') . ' WHERE user_id = ' . $user_id);
}
public function _save_login_in_session($user_info = [], $no_redirect_on_success = false)
Save auth information in session.
public function _save_login_in_session($user_info = [], $no_redirect_on_success = false)
{
if (empty($user_info['id'])) {
return js_redirect($this->URL_WRONG_LOGIN);
} elseif ($this->_is_user_inactive($user_info)) {
return js_redirect($this->URL_ACCOUNT_INACTIVE);
}
session_regenerate_id($destroy = true);
$_SESSION[$this->VAR_USER_ID] = $user_info['id'];
$_SESSION[$this->VAR_USER_GROUP_ID] = $user_info['group'];
$_SESSION[$this->VAR_USER_LOGIN_TIME] = time();
$_SESSION[$this->VAR_LOCK_IP] = common()->get_ip();
$_SESSION[$this->VAR_LOCK_UA] = ($_SERVER['HTTP_USER_AGENT'] ?? null);
$_SESSION[$this->VAR_LOCK_HOST] = ($_SERVER['HTTP_HOST'] ?? null);
$main = main();
$main->_init_cur_user_info($main);
$main->USER_INFO = &$main->_user_info;
$main->_LOGGED_IN_USER_INFO = &$main->_user_info;
if ($this->DO_LOG_LOGINS) {
_class('logs')->store_user_auth($user_info);
}
if ( ! empty($_POST[$this->REMEMBER_FIELD]) && $this->ALLOW_REMEMBER_ME) {
if ($this->REMEMBER_ME_TOKENS) {
$this->_remember_issue($user_info['id']);
} else {
$encrypted_string = _class('encryption')->_safe_encrypt_with_base64($user_info['id'] . '-' . $user_info[$this->LOGIN_FIELD] . '-' . $user_info[$this->PSWD_FIELD] . '-' . time());
$this->_cookie_set($this->VAR_COOKIE_NAME, $encrypted_string, time() + $this->VAR_COOKIE_LIFE_TIME);
}
}
// Set member id cookie expired on session end
if ($this->SET_MEMBER_ID_COOKIE && preg_match('/^[a-z0-9_\-]+$/ims', $this->SET_MEMBER_ID_COOKIE)) {
$this->_cookie_set($this->SET_MEMBER_ID_COOKIE, $user_info['id']);
}
if ($this->SET_IS_LOGGED_COOKIE && preg_match('/^[a-z0-9_\-]+$/ims', $this->SET_IS_LOGGED_COOKIE)) {
$this->_cookie_set($this->SET_IS_LOGGED_COOKIE, '1');
}
if ($this->CHECK_MULTI_ACCOUNTS) {
$this->_check_multi_accounts();
}
$this->_update_user_info_on_login($user_info);
$this->_update_online_info();
$this->_exec_method_on_action('login');
$group_info = [];
if ( ! empty($user_info['group'])) {
$groups = main()->get_data('user_groups_details');
$group_info = $groups[$user_info['group']];
}
if (empty($no_redirect_on_success)) {
$this->_success_login_redirect($user_info, $group_info);
}
if ($this->USER_INFO_IN_SESSION) {
$_SESSION[$this->VAR_USER_INFO] = $user_info;
}
return true;
}
public function _set_special_cookie($value = '')
Set cookie for the _check_multi_accounts().
public function _success_login_redirect($user_info = [], $group_info = [])
public function _success_login_redirect($user_info = [], $group_info = [])
{
$REDIRECT_URL = null;
if ($this->INSIDE_LOGIN_FROM_ADMIN) {
return js_redirect('/');
// Auto-redirect to the page before login form if needed
} elseif ( ! empty($_SESSION[$this->VAR_USER_GO_URL]) && ! ($this->URL_SUCCESS_LOGIN && ($_POST['skip_auto_url'] ?? null))) {
$REDIRECT_URL = (substr(($_SESSION[$this->VAR_USER_GO_URL] ?? ''), 0, 2) != './' ? './?' : '') . str_replace(WEB_PATH, '', str_replace(['http:', 'https:'], '', $_SESSION[$this->VAR_USER_GO_URL]));
$_SESSION[$this->VAR_USER_GO_URL] = '';
} elseif ( ! empty($user_info['go_after_login'])) {
$REDIRECT_URL = $user_info['go_after_login'];
} elseif ( ! empty($group_info['go_after_login'])) {
$REDIRECT_URL = $group_info['go_after_login'];
// Force redirect user to the default location
} elseif ( ! empty($this->URL_SUCCESS_LOGIN)) {
$REDIRECT_URL = $this->URL_SUCCESS_LOGIN;
}
if ($REDIRECT_URL) {
js_redirect($REDIRECT_URL);
}
}
public function _track_geo_location()
Do save geo location for guests.
public function _track_geo_location()
{
if ( ! $this->TRACK_GEO_LOCATION || ! main()->USE_GEO_IP) {
return false;
}
if ( ! ($_SESSION[$this->VAR_USER_ID] ?? null) && main()->SPIDERS_DETECTION) {
if (conf('SPIDER_NAME')) {
return false;
}
}
// Cleanup old-style geo location cookie
$old_cookie_name = 'geo_location';
if (isset($_COOKIE[$old_cookie_name])) {
$this->_cookie_del($old_cookie_name);
unset($_COOKIE[$old_cookie_name]);
}
$cur_ip = _es(common()->get_ip());
// Try to get data from cookie (current user selection)
$SEL_COOKIE_NAME = 'geo_selected';
if (isset($_COOKIE[$SEL_COOKIE_NAME]) && ! empty($_COOKIE[$SEL_COOKIE_NAME])) {
if ($this->GEO_COOKIE_JSON) {
$geo_data = $this->_geo_cookie_decode($_COOKIE[$SEL_COOKIE_NAME]);
$geo_data && $geo_data['_source'] = 'sel_cookie';
} else {
$geo_data = unserialize($_COOKIE[$SEL_COOKIE_NAME], ['allowed_classes' => false]);
$geo_data['_source'] = 'sel_cookie';
}
}
// Try to get data from nginx geoip module
if (empty($geo_data) && ! empty($_SERVER['HTTP_X_GEO_CITY'])) {
// Nginx currently not support lon/lat here:
/*
HTTP_X_GEO_CITY => 'Kiev',
HTTP_X_GEO_REGION => '13',
HTTP_X_GEO_COUNTRY
*/
}
// Try to get data from 'mod_geoip'
if (empty($geo_data) && ! empty($_SERVER['GEOIP_LATITUDE'])) {
$geo_data = [
'country_code' => ($_SERVER['GEOIP_COUNTRY_CODE'] ?? null),
'country_name' => ($_SERVER['GEOIP_COUNTRY_NAME'] ?? null),
'region_code' => ($_SERVER['GEOIP_REGION'] ?? null),
'city_name' => ($_SERVER['GEOIP_CITY'] ?? null),
'dma_code' => ($_SERVER['GEOIP_DMA_CODE'] ?? null),
'area_code' => ($_SERVER['GEOIP_AREA_CODE'] ?? null),
'longitude' => ($_SERVER['GEOIP_LONGITUDE'] ?? null),
'latitude' => ($_SERVER['GEOIP_LATITUDE'] ?? null),
];
$geo_data['_source'] = 'mod_geoip';
}
// Try to get data from 'geo_city_location' table
if (empty($geo_data)) {
$geo_data = common()->_get_geo_data_from_db($cur_ip);
$geo_data['_source'] = 'db_by_ip';
}
// Check data consistency
if ( ! empty($geo_data['city_name']) && ! strlen($geo_data['region_code'])) {
$geo_data['city_name'] = '';
}
if ( ! empty($geo_data['region_code']) && empty($geo_data['country_code'])) {
$geo_data['region_code'] = '';
$geo_data['region_name'] = '';
}
// Assign global array for use by all other code
main()->_USER_GEO_DATA = $geo_data;
}
public function _update_online_info()
Update online info.
public function _update_online_info()
{
if ( ! $this->STORE_ONLINE_USERS) {
return false;
}
if ( ! ($_SESSION[$this->VAR_USER_ID] ?? null) && main()->SPIDERS_DETECTION) {
if (conf('SPIDER_NAME')) {
return false;
}
}
$data = [
'id' => _es(session_id()),
'user_id' => (int) ($_SESSION[$this->VAR_USER_ID] ?? null),
'user_group' => (int) ($_SESSION[$this->VAR_USER_GROUP_ID] ?? null),
'time' => time(),
'type' => _es(MAIN_TYPE),
'ip' => _es(common()->get_ip()),
'user_agent' => _es($_SERVER['HTTP_USER_AGENT'] ?? null),
'query_string' => _es($_SERVER['QUERY_STRING'] ?? null),
'site_id' => (int) conf('SITE_ID'),
];
$sql = db()->REPLACE('online', $data, 1);
db()->_add_shutdown_query($sql);
return $data;
}
public function _update_user_info_on_login($user_info)
public function _update_user_info_on_login($user_info)
{
}
public function _user_security_checks($user_id)
public function _user_security_checks($user_id)
{
$user_id = (int) $user_id;
if ( ! $this->USER_SECURITY_CHECKS || ! $user_id) {
return false;
}
$fail_reason = false;
$cur_ip = common()->get_ip();
$cur_country = strtoupper(($_SERVER['GEOIP_COUNTRY_CODE'] ?? ''));
$user_settings = db()->from('user_settings')->whereid($user_id, 'user_id')->get_2d('key, value');
$fields_ip = ['ip_whitelist', 'ip_blacklist'];
$fields_country = ['country_whitelist', 'country_blacklist'];
foreach (array_merge($fields_ip, $fields_country) as $k) {
if ( ! strlen(trim($user_settings[$k] ?? ''))) {
continue;
}
$tmp = [];
foreach (explode(';', trim($user_settings[$k])) as $v) {
$v = trim($v);
$tmp[$v] = $v;
}
$user_settings[$k] = $tmp;
}
// Whitelists have more priority over blacklists, IP checks have more priority over country checks
$country_allow = null;
if ( ! empty($user_settings['country_whitelist'])) {
if (isset($user_settings['country_whitelist'][$cur_country])) {
$country_allow = true;
} else {
$country_allow = false;
}
} elseif ( ! empty($user_settings['country_blacklist']) && $cur_country && isset($user_settings['country_blacklist'][$cur_country])) {
$country_allow = false;
}
$ip_allow = null;
if ( ! empty($user_settings['ip_whitelist'])) {
if (isset($user_settings['ip_whitelist'][$cur_ip])) {
$ip_allow = true;
} else {
$ip_allow = false;
}
} elseif ( ! empty($user_settings['ip_blacklist']) && $cur_ip && isset($user_settings['ip_blacklist'][$cur_ip])) {
$ip_allow = false;
}
if ($ip_allow === false) {
$fail_reason = 'block_ip_user_settings';
$msg = 'Login from your IP (' . $cur_ip . ') is blocked by user security settings';
common()->message_error($msg);
trigger_error('AUTH: ' . $msg, E_USER_WARNING);
} elseif ($country_allow === false && $ip_allow !== true) {
$fail_reason = 'block_country_user_settings';
$msg = 'Login from your country (' . $cur_country . ') is blocked by user security settings';
common()->message_error($msg);
trigger_error('AUTH: ' . $msg, E_USER_WARNING);
}
return $fail_reason;
}