2020-09-22 06:04:33 +00:00
|
|
|
<?php
|
2021-02-26 16:16:17 +00:00
|
|
|
use OTPHP\TOTP;
|
|
|
|
|
2020-09-22 06:04:33 +00:00
|
|
|
class UserHelper {
|
|
|
|
|
2021-03-01 12:24:18 +00:00
|
|
|
const HASH_ALGO_SSHA512 = 'SSHA-512';
|
|
|
|
const HASH_ALGO_SSHA256 = 'SSHA-256';
|
|
|
|
const HASH_ALGO_MODE2 = 'MODE2';
|
|
|
|
const HASH_ALGO_SHA1X = 'SHA1X';
|
|
|
|
const HASH_ALGO_SHA1 = 'SHA1';
|
|
|
|
|
|
|
|
const HASH_ALGOS = [
|
|
|
|
self::HASH_ALGO_SSHA512,
|
|
|
|
self::HASH_ALGO_SSHA256,
|
|
|
|
self::HASH_ALGO_MODE2,
|
|
|
|
self::HASH_ALGO_SHA1X,
|
|
|
|
self::HASH_ALGO_SHA1
|
|
|
|
];
|
|
|
|
|
2021-11-10 17:44:51 +00:00
|
|
|
/** forbidden to login */
|
|
|
|
const ACCESS_LEVEL_DISABLED = -2;
|
|
|
|
|
|
|
|
/** can't subscribe to new feeds, feeds are not updated */
|
|
|
|
const ACCESS_LEVEL_READONLY = -1;
|
|
|
|
|
|
|
|
/** no restrictions, regular user */
|
|
|
|
const ACCESS_LEVEL_USER = 0;
|
|
|
|
|
|
|
|
/** not used, same as regular user */
|
|
|
|
const ACCESS_LEVEL_POWERUSER = 5;
|
|
|
|
|
|
|
|
/** has administrator permissions */
|
|
|
|
const ACCESS_LEVEL_ADMIN = 10;
|
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function authenticate(string $login = null, string $password = null, bool $check_only = false, string $service = null): bool {
|
2021-02-22 18:47:48 +00:00
|
|
|
if (!Config::get(Config::SINGLE_USER_MODE)) {
|
2020-09-22 06:04:33 +00:00
|
|
|
$user_id = false;
|
|
|
|
$auth_module = false;
|
|
|
|
|
2021-02-08 16:11:31 +00:00
|
|
|
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_AUTH_USER,
|
|
|
|
function ($result, $plugin) use (&$user_id, &$auth_module) {
|
|
|
|
if ($result) {
|
|
|
|
$user_id = (int)$result;
|
|
|
|
$auth_module = strtolower(get_class($plugin));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
$login, $password, $service);
|
2020-09-22 06:04:33 +00:00
|
|
|
|
|
|
|
if ($user_id && !$check_only) {
|
|
|
|
|
2021-02-25 06:33:36 +00:00
|
|
|
if (session_status() != PHP_SESSION_ACTIVE)
|
|
|
|
session_start();
|
|
|
|
|
2020-09-22 06:04:33 +00:00
|
|
|
session_regenerate_id(true);
|
|
|
|
|
2021-03-01 16:03:21 +00:00
|
|
|
$user = ORM::for_table('ttrss_users')->find_one($user_id);
|
|
|
|
|
2021-11-10 17:44:51 +00:00
|
|
|
if ($user && $user->access_level != self::ACCESS_LEVEL_DISABLED) {
|
2021-03-01 16:03:21 +00:00
|
|
|
$_SESSION["uid"] = $user_id;
|
|
|
|
$_SESSION["auth_module"] = $auth_module;
|
|
|
|
$_SESSION["name"] = $user->login;
|
|
|
|
$_SESSION["access_level"] = $user->access_level;
|
|
|
|
$_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
|
|
|
|
$_SESSION["ip_address"] = UserHelper::get_user_ip();
|
|
|
|
$_SESSION["pwd_hash"] = $user->pwd_hash;
|
|
|
|
|
2021-03-01 20:07:20 +00:00
|
|
|
$user->last_login = Db::NOW();
|
2021-03-01 16:03:21 +00:00
|
|
|
$user->save();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2020-09-22 06:04:33 +00:00
|
|
|
|
2021-03-01 16:03:21 +00:00
|
|
|
return false;
|
2020-09-22 06:04:33 +00:00
|
|
|
}
|
|
|
|
|
2021-02-25 15:26:37 +00:00
|
|
|
if ($login && $password && !$user_id && !$check_only)
|
2021-02-25 12:49:30 +00:00
|
|
|
Logger::log(E_USER_WARNING, "Failed login attempt for $login (service: $service) from " . UserHelper::get_user_ip());
|
2021-02-25 12:39:46 +00:00
|
|
|
|
2020-09-22 06:04:33 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
$_SESSION["uid"] = 1;
|
|
|
|
$_SESSION["name"] = "admin";
|
2021-11-10 17:44:51 +00:00
|
|
|
$_SESSION["access_level"] = self::ACCESS_LEVEL_ADMIN;
|
2020-09-22 06:04:33 +00:00
|
|
|
|
|
|
|
$_SESSION["hide_hello"] = true;
|
|
|
|
$_SESSION["hide_logout"] = true;
|
|
|
|
|
|
|
|
$_SESSION["auth_module"] = false;
|
|
|
|
|
2021-05-11 07:35:39 +00:00
|
|
|
if (empty($_SESSION["csrf_token"]))
|
2020-09-22 06:04:33 +00:00
|
|
|
$_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
|
|
|
|
|
2021-01-05 07:17:24 +00:00
|
|
|
$_SESSION["ip_address"] = UserHelper::get_user_ip();
|
2020-09-22 06:04:33 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function load_user_plugins(int $owner_uid, PluginHost $pluginhost = null): void {
|
2020-09-22 06:04:33 +00:00
|
|
|
|
|
|
|
if (!$pluginhost) $pluginhost = PluginHost::getInstance();
|
|
|
|
|
2021-03-02 12:03:01 +00:00
|
|
|
if ($owner_uid && Config::get_schema_version() >= 100 && empty($_SESSION["safe_mode"])) {
|
2021-02-25 11:49:58 +00:00
|
|
|
$plugins = get_pref(Prefs::_ENABLED_PLUGINS, $owner_uid);
|
2020-09-22 06:04:33 +00:00
|
|
|
|
2021-02-12 14:20:37 +00:00
|
|
|
$pluginhost->load((string)$plugins, PluginHost::KIND_USER, $owner_uid);
|
2020-09-22 06:04:33 +00:00
|
|
|
|
2021-01-15 05:32:06 +00:00
|
|
|
/*if (get_schema_version() > 100) {
|
2020-09-22 06:04:33 +00:00
|
|
|
$pluginhost->load_data();
|
2021-01-15 05:32:06 +00:00
|
|
|
}*/
|
2020-09-22 06:04:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function login_sequence(): void {
|
2020-09-22 06:04:33 +00:00
|
|
|
$pdo = Db::pdo();
|
|
|
|
|
2021-02-22 18:47:48 +00:00
|
|
|
if (Config::get(Config::SINGLE_USER_MODE)) {
|
2021-02-25 12:28:27 +00:00
|
|
|
if (session_status() != PHP_SESSION_ACTIVE)
|
|
|
|
session_start();
|
|
|
|
|
2020-09-22 11:54:15 +00:00
|
|
|
self::authenticate("admin", null);
|
2020-09-22 06:04:33 +00:00
|
|
|
startup_gettext();
|
2020-09-22 11:54:15 +00:00
|
|
|
self::load_user_plugins($_SESSION["uid"]);
|
2020-09-22 06:04:33 +00:00
|
|
|
} else {
|
2021-02-25 15:21:48 +00:00
|
|
|
if (!\Sessions\validate_session())
|
|
|
|
$_SESSION["uid"] = null;
|
2020-09-22 06:04:33 +00:00
|
|
|
|
2021-02-05 21:12:15 +00:00
|
|
|
if (empty($_SESSION["uid"])) {
|
2020-09-22 06:04:33 +00:00
|
|
|
|
2021-02-22 18:47:48 +00:00
|
|
|
if (Config::get(Config::AUTH_AUTO_LOGIN) && self::authenticate(null, null)) {
|
2021-02-25 12:49:30 +00:00
|
|
|
$_SESSION["ref_schema_version"] = get_schema_version();
|
2020-09-22 06:04:33 +00:00
|
|
|
} else {
|
2020-09-22 11:54:15 +00:00
|
|
|
self::authenticate(null, null, true);
|
2020-09-22 06:04:33 +00:00
|
|
|
}
|
|
|
|
|
2021-02-05 21:12:15 +00:00
|
|
|
if (empty($_SESSION["uid"])) {
|
2021-02-14 12:31:03 +00:00
|
|
|
UserHelper::logout();
|
2020-09-22 06:04:33 +00:00
|
|
|
|
2021-02-19 17:21:36 +00:00
|
|
|
Handler_Public::_render_login_form();
|
2020-09-22 06:04:33 +00:00
|
|
|
exit;
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
/* bump login timestamp */
|
2021-03-01 16:32:27 +00:00
|
|
|
$user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]);
|
2021-03-01 20:07:20 +00:00
|
|
|
$user->last_login = Db::NOW();
|
2021-03-01 16:32:27 +00:00
|
|
|
$user->save();
|
2020-09-22 06:04:33 +00:00
|
|
|
|
|
|
|
$_SESSION["last_login_update"] = time();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($_SESSION["uid"]) {
|
|
|
|
startup_gettext();
|
2020-09-22 11:54:15 +00:00
|
|
|
self::load_user_plugins($_SESSION["uid"]);
|
2020-09-22 06:04:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function print_user_stylesheet(): void {
|
2021-02-25 11:49:58 +00:00
|
|
|
$value = get_pref(Prefs::USER_STYLESHEET);
|
2020-09-22 06:04:33 +00:00
|
|
|
|
|
|
|
if ($value) {
|
|
|
|
print "<style type='text/css' id='user_css_style'>";
|
|
|
|
print str_replace("<br/>", "\n", $value);
|
|
|
|
print "</style>";
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function get_user_ip(): ?string {
|
2021-01-05 07:25:43 +00:00
|
|
|
foreach (["HTTP_X_REAL_IP", "REMOTE_ADDR"] as $hdr) {
|
2021-01-05 07:17:24 +00:00
|
|
|
if (isset($_SERVER[$hdr]))
|
|
|
|
return $_SERVER[$hdr];
|
|
|
|
}
|
2021-02-26 16:16:17 +00:00
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function get_login_by_id(int $id): ?string {
|
2021-03-01 16:03:21 +00:00
|
|
|
$user = ORM::for_table('ttrss_users')
|
|
|
|
->find_one($id);
|
2021-02-26 16:16:17 +00:00
|
|
|
|
2021-03-01 16:03:21 +00:00
|
|
|
if ($user)
|
|
|
|
return $user->login;
|
|
|
|
else
|
|
|
|
return null;
|
2021-01-05 07:17:24 +00:00
|
|
|
}
|
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function find_user_by_login(string $login): ?int {
|
2021-03-01 16:03:21 +00:00
|
|
|
$user = ORM::for_table('ttrss_users')
|
|
|
|
->where('login', $login)
|
|
|
|
->find_one();
|
2021-02-11 07:22:27 +00:00
|
|
|
|
2021-03-01 16:03:21 +00:00
|
|
|
if ($user)
|
|
|
|
return $user->id;
|
|
|
|
else
|
|
|
|
return null;
|
2021-02-11 07:22:27 +00:00
|
|
|
}
|
2021-02-14 12:31:03 +00:00
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function logout(): void {
|
2021-02-14 12:31:03 +00:00
|
|
|
if (session_status() === PHP_SESSION_ACTIVE)
|
|
|
|
session_destroy();
|
|
|
|
|
|
|
|
if (isset($_COOKIE[session_name()])) {
|
|
|
|
setcookie(session_name(), '', time()-42000, '/');
|
|
|
|
|
|
|
|
}
|
|
|
|
session_commit();
|
|
|
|
}
|
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function get_salt(): string {
|
2021-03-01 12:24:18 +00:00
|
|
|
return substr(bin2hex(get_random_bytes(125)), 0, 250);
|
|
|
|
}
|
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function reset_password(int $uid, bool $format_output = false, string $new_password = ""): void {
|
2021-02-15 13:59:54 +00:00
|
|
|
|
2021-03-01 16:32:27 +00:00
|
|
|
$user = ORM::for_table('ttrss_users')->find_one($uid);
|
|
|
|
$message = "";
|
2021-02-15 13:59:54 +00:00
|
|
|
|
2021-03-01 16:32:27 +00:00
|
|
|
if ($user) {
|
2021-02-15 13:59:54 +00:00
|
|
|
|
2021-03-01 16:32:27 +00:00
|
|
|
$login = $user->login;
|
2021-02-15 13:59:54 +00:00
|
|
|
|
2021-03-01 12:24:18 +00:00
|
|
|
$new_salt = self::get_salt();
|
|
|
|
$tmp_user_pwd = $new_password ? $new_password : make_password();
|
2021-02-15 13:59:54 +00:00
|
|
|
|
2021-03-01 12:24:18 +00:00
|
|
|
$pwd_hash = self::hash_password($tmp_user_pwd, $new_salt, self::HASH_ALGOS[0]);
|
2021-02-15 13:59:54 +00:00
|
|
|
|
2021-03-01 16:32:27 +00:00
|
|
|
$user->pwd_hash = $pwd_hash;
|
|
|
|
$user->salt = $new_salt;
|
|
|
|
$user->save();
|
2021-02-15 13:59:54 +00:00
|
|
|
|
2021-03-01 16:32:27 +00:00
|
|
|
$message = T_sprintf("Changed password of user %s to %s", "<strong>$login</strong>", "<strong>$tmp_user_pwd</strong>");
|
|
|
|
} else {
|
|
|
|
$message = __("User not found");
|
2021-02-15 13:59:54 +00:00
|
|
|
}
|
2021-03-01 16:32:27 +00:00
|
|
|
|
|
|
|
if ($format_output)
|
|
|
|
print_notice($message);
|
|
|
|
else
|
|
|
|
print $message;
|
2021-02-15 13:59:54 +00:00
|
|
|
}
|
2021-02-26 16:16:17 +00:00
|
|
|
|
|
|
|
static function check_otp(int $owner_uid, int $otp_check) : bool {
|
|
|
|
$otp = TOTP::create(self::get_otp_secret($owner_uid, true));
|
|
|
|
|
|
|
|
return $otp->now() == $otp_check;
|
|
|
|
}
|
|
|
|
|
|
|
|
static function disable_otp(int $owner_uid) : bool {
|
2021-03-01 16:03:21 +00:00
|
|
|
$user = ORM::for_table('ttrss_users')->find_one($owner_uid);
|
2021-02-26 16:16:17 +00:00
|
|
|
|
2021-03-01 16:03:21 +00:00
|
|
|
if ($user) {
|
|
|
|
$user->otp_enabled = false;
|
2021-03-05 14:40:17 +00:00
|
|
|
|
|
|
|
// force new OTP secret when next enabled
|
|
|
|
if (Config::get_schema_version() >= 143) {
|
|
|
|
$user->otp_secret = null;
|
|
|
|
}
|
|
|
|
|
2021-03-01 16:03:21 +00:00
|
|
|
$user->save();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
2021-02-26 16:16:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static function enable_otp(int $owner_uid, int $otp_check) : bool {
|
|
|
|
$secret = self::get_otp_secret($owner_uid);
|
|
|
|
|
|
|
|
if ($secret) {
|
|
|
|
$otp = TOTP::create($secret);
|
2021-03-01 16:03:21 +00:00
|
|
|
$user = ORM::for_table('ttrss_users')->find_one($owner_uid);
|
2021-02-26 16:16:17 +00:00
|
|
|
|
2021-03-01 16:03:21 +00:00
|
|
|
if ($otp->now() == $otp_check && $user) {
|
2021-02-26 16:16:17 +00:00
|
|
|
|
2021-03-01 16:03:21 +00:00
|
|
|
$user->otp_enabled = true;
|
|
|
|
$user->save();
|
2021-02-26 16:16:17 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static function is_otp_enabled(int $owner_uid) : bool {
|
2021-03-01 16:03:21 +00:00
|
|
|
$user = ORM::for_table('ttrss_users')->find_one($owner_uid);
|
2021-02-26 16:16:17 +00:00
|
|
|
|
2021-03-01 16:03:21 +00:00
|
|
|
if ($user) {
|
|
|
|
return $user->otp_enabled;
|
|
|
|
} else {
|
|
|
|
return false;
|
2021-02-26 16:16:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function get_otp_secret(int $owner_uid, bool $show_if_enabled = false): ?string {
|
2021-03-01 16:03:21 +00:00
|
|
|
$user = ORM::for_table('ttrss_users')->find_one($owner_uid);
|
2021-02-26 16:16:17 +00:00
|
|
|
|
2021-03-01 16:03:21 +00:00
|
|
|
if ($user) {
|
2021-03-05 14:40:17 +00:00
|
|
|
|
|
|
|
$salt_based_secret = mb_substr(sha1($user->salt), 0, 12);
|
|
|
|
|
|
|
|
if (Config::get_schema_version() >= 143) {
|
|
|
|
$secret = $user->otp_secret;
|
|
|
|
|
|
|
|
if (empty($secret)) {
|
|
|
|
|
|
|
|
/* migrate secret if OTP is already enabled, otherwise make a new one */
|
|
|
|
if ($user->otp_enabled) {
|
|
|
|
$user->otp_secret = $salt_based_secret;
|
|
|
|
} else {
|
2021-03-29 16:22:03 +00:00
|
|
|
$user->otp_secret = bin2hex(get_random_bytes(10));
|
2021-03-05 14:40:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
$user->save();
|
|
|
|
|
|
|
|
$secret = $user->otp_secret;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$secret = $salt_based_secret;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$user->otp_enabled || $show_if_enabled) {
|
|
|
|
return \ParagonIE\ConstantTime\Base32::encodeUpperUnpadded($secret);
|
|
|
|
}
|
2021-02-26 16:16:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
2021-03-01 12:24:18 +00:00
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
static function is_default_password(): bool {
|
2021-03-01 12:24:18 +00:00
|
|
|
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
|
|
|
|
|
|
|
|
if ($authenticator &&
|
|
|
|
method_exists($authenticator, "check_password") &&
|
|
|
|
$authenticator->check_password($_SESSION["uid"], "password")) {
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-11-11 20:25:13 +00:00
|
|
|
/**
|
|
|
|
* @param UserHelper::HASH_ALGO_* $algo
|
|
|
|
*
|
|
|
|
* @return false|string False if the password couldn't be hashed, otherwise the hash string.
|
|
|
|
*/
|
|
|
|
static function hash_password(string $pass, string $salt, string $algo = self::HASH_ALGOS[0]) {
|
2021-03-01 12:24:18 +00:00
|
|
|
$pass_hash = "";
|
|
|
|
|
|
|
|
switch ($algo) {
|
|
|
|
case self::HASH_ALGO_SHA1:
|
|
|
|
$pass_hash = sha1($pass);
|
|
|
|
break;
|
|
|
|
case self::HASH_ALGO_SHA1X:
|
|
|
|
$pass_hash = sha1("$salt:$pass");
|
|
|
|
break;
|
|
|
|
case self::HASH_ALGO_MODE2:
|
|
|
|
case self::HASH_ALGO_SSHA256:
|
|
|
|
$pass_hash = hash('sha256', $salt . $pass);
|
|
|
|
break;
|
|
|
|
case self::HASH_ALGO_SSHA512:
|
|
|
|
$pass_hash = hash('sha512', $salt . $pass);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
user_error("hash_password: unknown hash algo: $algo", E_USER_ERROR);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($pass_hash)
|
|
|
|
return "$algo:$pass_hash";
|
|
|
|
else
|
|
|
|
return false;
|
|
|
|
}
|
2020-09-22 06:04:33 +00:00
|
|
|
}
|