add 2FA mechanism, fixes #547
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
87
2fa.php
Normal file
87
2fa.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
if (! defined('AREA')) {
|
||||
header("Location: index.php");
|
||||
exit();
|
||||
}
|
||||
if (Settings::Get('2fa.enabled') != '1') {
|
||||
dynamic_error("2FA not activated");
|
||||
}
|
||||
|
||||
/**
|
||||
* This file is part of the Froxlor project.
|
||||
* Copyright (c) 2018 the Froxlor Team (see authors).
|
||||
*
|
||||
* For the full copyright and license information, please view the COPYING
|
||||
* file that was distributed with this source code. You can also view the
|
||||
* COPYING file online at http://files.froxlor.org/misc/COPYING.txt
|
||||
*
|
||||
* @copyright (c) the authors
|
||||
* @author Froxlor team <team@froxlor.org> (2018-)
|
||||
* @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
|
||||
* @package Panel
|
||||
* @since 0.10.0
|
||||
*
|
||||
*/
|
||||
|
||||
// This file is being included in admin_index and customer_index
|
||||
// and therefore does not need to require lib/init.php
|
||||
if (AREA == 'admin') {
|
||||
$upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_ADMINS . "` SET `type_2fa` = :t2fa, `data_2fa` = :d2fa WHERE adminid = :id");
|
||||
$uid = $userinfo['adminid'];
|
||||
} elseif (AREA == 'customer') {
|
||||
$upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `type_2fa` = :t2fa, `data_2fa` = :d2fa WHERE customerid = :id");
|
||||
$uid = $userinfo['customerid'];
|
||||
}
|
||||
$success_message = "";
|
||||
|
||||
$tfa = new FroxlorTwoFactorAuth('Froxlor');
|
||||
|
||||
// do the delete and then just show a success-message
|
||||
if ($action == 'delete') {
|
||||
Database::pexecute($upd_stmt, array(
|
||||
't2fa' => 0,
|
||||
'd2fa' => "",
|
||||
'id' => $uid
|
||||
));
|
||||
standard_success($lng['2fa']['2fa_removed']);
|
||||
} elseif ($action == 'add') {
|
||||
$type = isset($_POST['type_2fa']) ? $_POST['type_2fa'] : '0';
|
||||
|
||||
if ($type == 0 || $type == 1) {
|
||||
$data = "";
|
||||
}
|
||||
if ($type == 2) {
|
||||
// generate secret for TOTP
|
||||
$data = $tfa->createSecret();
|
||||
}
|
||||
Database::pexecute($upd_stmt, array(
|
||||
't2fa' => $type,
|
||||
'd2fa' => $data,
|
||||
'id' => $uid
|
||||
));
|
||||
standard_success($lng['2fa']['2fa_added']);
|
||||
}
|
||||
|
||||
$log->logAction(USR_ACTION, LOG_NOTICE, "viewed 2fa::overview");
|
||||
|
||||
if ($userinfo['type_2fa'] == '0') {
|
||||
|
||||
// available types
|
||||
$type_select_values = array(
|
||||
0 => '-',
|
||||
1 => 'E-Mail',
|
||||
2 => 'Authenticator'
|
||||
);
|
||||
asort($type_select_values);
|
||||
foreach ($type_select_values as $_val => $_type) {
|
||||
$type_select .= makeoption($_type, $_val);
|
||||
}
|
||||
}
|
||||
elseif ($userinfo['type_2fa'] == '1') {
|
||||
// email 2fa enabled
|
||||
}
|
||||
elseif ($userinfo['type_2fa'] == '2') {
|
||||
// authenticator 2fa enabled
|
||||
$ga_qrcode = $tfa->getQRCodeImageAsDataUri($userinfo['loginname'], $userinfo['data_2fa']);
|
||||
}
|
||||
eval("echo \"" . getTemplate("2fa/overview", true) . "\";");
|
||||
@@ -62,6 +62,14 @@ return array(
|
||||
'default' => 900,
|
||||
'save_method' => 'storeSettingField',
|
||||
),
|
||||
'2fa_enabled' => array(
|
||||
'label' => $lng['2fa']['2fa_enabled'],
|
||||
'settinggroup' => '2fa',
|
||||
'varname' => 'enabled',
|
||||
'type' => 'bool',
|
||||
'default' => true,
|
||||
'save_method' => 'storeSettingField',
|
||||
),
|
||||
'panel_password_min_length' => array(
|
||||
'label' => $lng['serversettings']['panel_password_min_length'],
|
||||
'settinggroup' => 'panel',
|
||||
|
||||
542
index.php
542
index.php
@@ -16,7 +16,6 @@
|
||||
* @package Panel
|
||||
*
|
||||
*/
|
||||
|
||||
define('AREA', 'login');
|
||||
require './lib/init.php';
|
||||
|
||||
@@ -24,15 +23,89 @@ if ($action == '') {
|
||||
$action = 'login';
|
||||
}
|
||||
|
||||
if ($action == 'login') {
|
||||
session_start();
|
||||
|
||||
if ($action == '2fa_entercode') {
|
||||
// page for entering the 2FA code after successful login
|
||||
if (! isset($_SESSION) || ! isset($_SESSION['secret_2fa'])) {
|
||||
// no session - redirect to index
|
||||
redirectTo('index.php');
|
||||
exit();
|
||||
}
|
||||
// show template to enter code
|
||||
eval("echo \"" . getTemplate('2fa/entercode', true) . "\";");
|
||||
} elseif ($action == '2fa_verify') {
|
||||
// verify code from 2fa code-enter form
|
||||
if (! isset($_SESSION) || ! isset($_SESSION['secret_2fa'])) {
|
||||
// no session - redirect to index
|
||||
redirectTo('index.php');
|
||||
exit();
|
||||
}
|
||||
$code = isset($_POST['2fa_code']) ? $_POST['2fa_code'] : null;
|
||||
// verify entered code
|
||||
$tfa = new FroxlorTwoFactorAuth('Froxlor');
|
||||
$result = ($_SESSION['secret_2fa'] == 'email' ? true : $tfa->verifyCode($_SESSION['secret_2fa'], $code, 3));
|
||||
// either the code is valid when using authenticator-app, or we will select userdata by id and entered code
|
||||
// which is temporarily stored for the customer when using email-2fa
|
||||
if ($result) {
|
||||
// get user-data
|
||||
$table = $_SESSION['uidtable_2fa'];
|
||||
$field = $_SESSION['uidfield_2fa'];
|
||||
$uid = $_SESSION['uid_2fa'];
|
||||
$isadmin = $_SESSION['unfo_2fa'];
|
||||
$sel_param = array(
|
||||
'uid' => $uid
|
||||
);
|
||||
if ($_SESSION['secret_2fa'] == 'email') {
|
||||
// verify code by selecting user by id and the temp. stored code,
|
||||
// so only if it's the correct code, we get the user-data
|
||||
$sel_stmt = Database::prepare("SELECT * FROM $table WHERE `" . $field . "` = :uid AND `data_2fa` = :code");
|
||||
$sel_param['code'] = $code;
|
||||
} else {
|
||||
// Authenticator-verification has already happened at this point, so just get the user-data
|
||||
$sel_stmt = Database::prepare("SELECT * FROM $table WHERE `" . $field . "` = :uid");
|
||||
}
|
||||
$userinfo = Database::pexecute_first($sel_stmt, $sel_param);
|
||||
// whoops, no (valid) user? Start again
|
||||
if (empty($userinfo)) {
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '2'
|
||||
));
|
||||
}
|
||||
// set fields in $userinfo required for finishLogin()
|
||||
$userinfo['adminsession'] = $isadmin;
|
||||
$userinfo['userid'] = $uid;
|
||||
|
||||
// if not successful somehow - start again
|
||||
if (! finishLogin($userinfo)) {
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '2'
|
||||
));
|
||||
}
|
||||
|
||||
// when using email-2fa, remove the one-time-code
|
||||
if ($userinfo['type_2fa'] == '1') {
|
||||
$del_stmt = Database::prepare("UPDATE $table SET `data_2fa` = '' WHERE `" . $field . "` = :uid");
|
||||
$userinfo = Database::pexecute_first($del_stmt, array(
|
||||
'uid' => $uid
|
||||
));
|
||||
}
|
||||
exit();
|
||||
}
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '2'
|
||||
));
|
||||
exit();
|
||||
} elseif ($action == 'login') {
|
||||
if (isset($_POST['send']) && $_POST['send'] == 'send') {
|
||||
$loginname = validate($_POST['loginname'], 'loginname');
|
||||
$password = validate($_POST['password'], 'password');
|
||||
|
||||
$stmt = Database::prepare("SELECT `loginname` AS `customer` FROM `" . TABLE_PANEL_CUSTOMERS . "`
|
||||
WHERE `loginname`= :loginname"
|
||||
);
|
||||
Database::pexecute($stmt, array("loginname" => $loginname));
|
||||
WHERE `loginname`= :loginname");
|
||||
Database::pexecute($stmt, array(
|
||||
"loginname" => $loginname
|
||||
));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($row['customer'] == $loginname) {
|
||||
@@ -43,20 +116,25 @@ if ($action == 'login') {
|
||||
} else {
|
||||
$is_admin = true;
|
||||
if ((int) Settings::Get('login.domain_login') == 1) {
|
||||
$domainname = $idna_convert->encode(preg_replace(array('/\:(\d)+$/', '/^https?\:\/\//'), '', $loginname));
|
||||
$domainname = $idna_convert->encode(preg_replace(array(
|
||||
'/\:(\d)+$/',
|
||||
'/^https?\:\/\//'
|
||||
), '', $loginname));
|
||||
$stmt = Database::prepare("SELECT `customerid` FROM `" . TABLE_PANEL_DOMAINS . "`
|
||||
WHERE `domain` = :domain"
|
||||
);
|
||||
Database::pexecute($stmt, array("domain" => $domainname));
|
||||
WHERE `domain` = :domain");
|
||||
Database::pexecute($stmt, array(
|
||||
"domain" => $domainname
|
||||
));
|
||||
$row2 = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (isset($row2['customerid']) && $row2['customerid'] > 0) {
|
||||
$loginname = getCustomerDetail($row2['customerid'], 'loginname');
|
||||
if ($loginname !== false) {
|
||||
$stmt = Database::prepare("SELECT `loginname` AS `customer` FROM `" . TABLE_PANEL_CUSTOMERS . "`
|
||||
WHERE `loginname`= :loginname"
|
||||
);
|
||||
Database::pexecute($stmt, array("loginname" => $loginname));
|
||||
WHERE `loginname`= :loginname");
|
||||
Database::pexecute($stmt, array(
|
||||
"loginname" => $loginname
|
||||
));
|
||||
$row3 = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row3['customer'] == $loginname) {
|
||||
$table = "`" . TABLE_PANEL_CUSTOMERS . "`";
|
||||
@@ -71,27 +149,29 @@ if ($action == 'login') {
|
||||
|
||||
if ((hasUpdates($version) || hasDbUpdates($dbversion)) && $is_admin == false) {
|
||||
redirectTo('index.php');
|
||||
exit;
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($is_admin) {
|
||||
if (hasUpdates($version) || hasDbUpdates($dbversion)) {
|
||||
$stmt = Database::prepare("SELECT `loginname` AS `admin` FROM `" . TABLE_PANEL_ADMINS . "`
|
||||
WHERE `loginname`= :loginname
|
||||
AND `change_serversettings` = '1'"
|
||||
);
|
||||
Database::pexecute($stmt, array("loginname" => $loginname));
|
||||
AND `change_serversettings` = '1'");
|
||||
Database::pexecute($stmt, array(
|
||||
"loginname" => $loginname
|
||||
));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (! isset($row['admin'])) {
|
||||
// not an admin who can see updates
|
||||
redirectTo('index.php');
|
||||
exit;
|
||||
exit();
|
||||
}
|
||||
} else {
|
||||
$stmt = Database::prepare("SELECT `loginname` AS `admin` FROM `" . TABLE_PANEL_ADMINS . "`
|
||||
WHERE `loginname`= :loginname"
|
||||
);
|
||||
Database::pexecute($stmt, array("loginname" => $loginname));
|
||||
WHERE `loginname`= :loginname");
|
||||
Database::pexecute($stmt, array(
|
||||
"loginname" => $loginname
|
||||
));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
@@ -101,38 +181,49 @@ if ($action == 'login') {
|
||||
$adminsession = '1';
|
||||
} else {
|
||||
// Log failed login
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array('loginname' => $_SERVER['REMOTE_ADDR']));
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => $_SERVER['REMOTE_ADDR']
|
||||
));
|
||||
$rstlog->logAction(LOGIN_ACTION, LOG_WARNING, "Unknown user '" . $loginname . "' tried to login.");
|
||||
|
||||
redirectTo('index.php', array('showmessage' => '2'));
|
||||
exit;
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '2'
|
||||
));
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$userinfo_stmt = Database::prepare("SELECT * FROM $table
|
||||
WHERE `loginname`= :loginname"
|
||||
);
|
||||
Database::pexecute($userinfo_stmt, array("loginname" => $loginname));
|
||||
WHERE `loginname`= :loginname");
|
||||
Database::pexecute($userinfo_stmt, array(
|
||||
"loginname" => $loginname
|
||||
));
|
||||
$userinfo = $userinfo_stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($userinfo['loginfail_count'] >= Settings::Get('login.maxloginattempts') && $userinfo['lastlogin_fail'] > (time() - Settings::Get('login.deactivatetime'))) {
|
||||
redirectTo('index.php', array('showmessage' => '3'));
|
||||
exit;
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '3'
|
||||
));
|
||||
exit();
|
||||
} elseif (validatePasswordLogin($userinfo, $password, $table, $uid)) {
|
||||
// only show "you're banned" if the login was successful
|
||||
// because we don't want to publish that the user does exist
|
||||
if ($userinfo['deactivated']) {
|
||||
unset($userinfo);
|
||||
redirectTo('index.php', array('showmessage' => '5'));
|
||||
exit;
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '5'
|
||||
));
|
||||
exit();
|
||||
} else {
|
||||
// login correct
|
||||
// reset loginfail_counter, set lastlogin_succ
|
||||
$stmt = Database::prepare("UPDATE $table
|
||||
SET `lastlogin_succ`= :lastlogin_succ, `loginfail_count`='0'
|
||||
WHERE `$uid`= :uid"
|
||||
);
|
||||
Database::pexecute($stmt, array("lastlogin_succ" => time(), "uid" => $userinfo[$uid]));
|
||||
WHERE `$uid`= :uid");
|
||||
Database::pexecute($stmt, array(
|
||||
"lastlogin_succ" => time(),
|
||||
"uid" => $userinfo[$uid]
|
||||
));
|
||||
$userinfo['userid'] = $userinfo[$uid];
|
||||
$userinfo['adminsession'] = $adminsession;
|
||||
}
|
||||
@@ -140,112 +231,92 @@ if ($action == 'login') {
|
||||
// login incorrect
|
||||
$stmt = Database::prepare("UPDATE $table
|
||||
SET `lastlogin_fail`= :lastlogin_fail, `loginfail_count`=`loginfail_count`+1
|
||||
WHERE `$uid`= :uid"
|
||||
);
|
||||
Database::pexecute($stmt, array("lastlogin_fail" => time(), "uid" => $userinfo[$uid]));
|
||||
WHERE `$uid`= :uid");
|
||||
Database::pexecute($stmt, array(
|
||||
"lastlogin_fail" => time(),
|
||||
"uid" => $userinfo[$uid]
|
||||
));
|
||||
|
||||
// Log failed login
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array('loginname' => $_SERVER['REMOTE_ADDR']));
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => $_SERVER['REMOTE_ADDR']
|
||||
));
|
||||
$rstlog->logAction(LOGIN_ACTION, LOG_WARNING, "User '" . $loginname . "' tried to login with wrong password.");
|
||||
|
||||
unset($userinfo);
|
||||
redirectTo('index.php', array('showmessage' => '2'));
|
||||
exit;
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '2'
|
||||
));
|
||||
exit();
|
||||
}
|
||||
|
||||
if (isset($userinfo['userid']) && $userinfo['userid'] != '') {
|
||||
$s = md5(uniqid(microtime(), 1));
|
||||
|
||||
if (isset($_POST['language'])) {
|
||||
$language = validate($_POST['language'], 'language');
|
||||
if ($language == 'profile') {
|
||||
$language = $userinfo['def_language'];
|
||||
} elseif (!isset($languages[$language])) {
|
||||
$language = Settings::Get('panel.standardlanguage');
|
||||
}
|
||||
} else {
|
||||
$language = Settings::Get('panel.standardlanguage');
|
||||
}
|
||||
|
||||
if (isset($userinfo['theme']) && $userinfo['theme'] != '') {
|
||||
$theme = $userinfo['theme'];
|
||||
} else {
|
||||
$theme = Settings::Get('panel.default_theme');
|
||||
}
|
||||
|
||||
if (Settings::Get('session.allow_multiple_login') != '1') {
|
||||
$stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_SESSIONS . "`
|
||||
WHERE `userid` = :uid
|
||||
AND `adminsession` = :adminsession"
|
||||
// 2FA activated
|
||||
if (Settings::Get('2fa.enabled') == '1' && $userinfo['type_2fa'] > 0) {
|
||||
// redirect to code-enter-page
|
||||
$_SESSION['secret_2fa'] = ($userinfo['type_2fa'] == 2 ? $userinfo['data_2fa'] : 'email');
|
||||
$_SESSION['uid_2fa'] = $userinfo[$uid];
|
||||
$_SESSION['uidfield_2fa'] = $uid;
|
||||
$_SESSION['uidtable_2fa'] = $table;
|
||||
$_SESSION['unfo_2fa'] = $is_admin;
|
||||
// send mail if type_2fa = 1 (email)
|
||||
if ($userinfo['type_2fa'] == 1) {
|
||||
// generate code
|
||||
$tfa = new FroxlorTwoFactorAuth('Froxlor');
|
||||
$code = $tfa->getCode($tfa->createSecret());
|
||||
// set code for user
|
||||
$stmt = Database::prepare("UPDATE $table SET `data_2fa` = :d2fa WHERE `$uid` = :uid");
|
||||
Database::pexecute($stmt, array(
|
||||
"d2fa" => $code,
|
||||
"uid" => $userinfo[$uid]
|
||||
));
|
||||
// build up & send email
|
||||
$_mailerror = false;
|
||||
$mailerr_msg = "";
|
||||
$replace_arr = array(
|
||||
'CODE' => $code
|
||||
);
|
||||
Database::pexecute($stmt, array("uid" => $userinfo['userid'], "adminsession" => $userinfo['adminsession']));
|
||||
$mail_body = html_entity_decode(replace_variables($lng['mails']['2fa']['mailbody'], $replace_arr));
|
||||
|
||||
try {
|
||||
$mail->Subject = $lng['mails']['2fa']['subject'];
|
||||
$mail->AltBody = $mail_body;
|
||||
$mail->MsgHTML(str_replace("\n", "<br />", $mail_body));
|
||||
$mail->AddAddress($userinfo['email'], getCorrectUserSalutation($userinfo));
|
||||
$mail->Send();
|
||||
} catch (phpmailerException $e) {
|
||||
$mailerr_msg = $e->errorMessage();
|
||||
$_mailerror = true;
|
||||
} catch (Exception $e) {
|
||||
$mailerr_msg = $e->getMessage();
|
||||
$_mailerror = true;
|
||||
}
|
||||
|
||||
// check for field 'theme' in session-table, refs #607
|
||||
// Changed with #1287 to new method
|
||||
$theme_field = false;
|
||||
$stmt = Database::query("SHOW COLUMNS FROM panel_sessions LIKE 'theme'");
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
if ($row['Field'] == "theme") {
|
||||
$has_theme = true;
|
||||
}
|
||||
if ($_mailerror) {
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => '2fa code-sending'
|
||||
));
|
||||
$rstlog->logAction(ADM_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg);
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '4',
|
||||
'customermail' => $userinfo['email']
|
||||
));
|
||||
exit();
|
||||
}
|
||||
|
||||
$params = array(
|
||||
"hash" => $s,
|
||||
"userid" => $userinfo['userid'],
|
||||
"ipaddress" => $remote_addr,
|
||||
"useragent" => $http_user_agent,
|
||||
"lastactivity" => time(),
|
||||
"language" => $language,
|
||||
"adminsession" => $userinfo['adminsession']
|
||||
);
|
||||
$mail->ClearAddresses();
|
||||
}
|
||||
redirectTo('index.php', array(
|
||||
'action' => '2fa_entercode'
|
||||
));
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($has_theme) {
|
||||
$params["theme"] = $theme;
|
||||
$stmt = Database::prepare("INSERT INTO `" . TABLE_PANEL_SESSIONS . "`
|
||||
(`hash`, `userid`, `ipaddress`, `useragent`, `lastactivity`, `language`, `adminsession`, `theme`)
|
||||
VALUES (:hash, :userid, :ipaddress, :useragent, :lastactivity, :language, :adminsession, :theme)"
|
||||
);
|
||||
} else {
|
||||
$stmt = Database::prepare("INSERT INTO `" . TABLE_PANEL_SESSIONS . "`
|
||||
(`hash`, `userid`, `ipaddress`, `useragent`, `lastactivity`, `language`, `adminsession`)
|
||||
VALUES (:hash, :userid, :ipaddress, :useragent, :lastactivity, :language, :adminsession)"
|
||||
);
|
||||
if (! finishLogin($userinfo)) {
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '2'
|
||||
));
|
||||
}
|
||||
Database::pexecute($stmt, $params);
|
||||
|
||||
$qryparams = array();
|
||||
if (isset($_POST['qrystr']) && $_POST['qrystr'] != "") {
|
||||
parse_str(urldecode($_POST['qrystr']), $qryparams);
|
||||
}
|
||||
$qryparams['s'] = $s;
|
||||
|
||||
if ($userinfo['adminsession'] == '1') {
|
||||
if (hasUpdates($version) || hasDbUpdates($dbversion)) {
|
||||
redirectTo('admin_updates.php', array('s' => $s));
|
||||
} else {
|
||||
if (isset($_POST['script']) && $_POST['script'] != "") {
|
||||
if (preg_match("/customer\_/", $_POST['script']) === 1) {
|
||||
redirectTo('admin_customers.php', array("page" => "customers"));
|
||||
} else {
|
||||
redirectTo($_POST['script'], $qryparams);
|
||||
}
|
||||
} else {
|
||||
redirectTo('admin_index.php', $qryparams);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isset($_POST['script']) && $_POST['script'] != "") {
|
||||
redirectTo($_POST['script'], $qryparams);
|
||||
} else {
|
||||
redirectTo('customer_index.php', $qryparams);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
redirectTo('index.php', array('showmessage' => '2'));
|
||||
}
|
||||
exit;
|
||||
exit();
|
||||
} else {
|
||||
$language_options = '';
|
||||
$language_options .= makeoption($lng['login']['profile_lng'], 'profile', 'profile', true, true);
|
||||
@@ -318,16 +389,20 @@ if ($action == 'forgotpwd') {
|
||||
$email = validateEmail($_POST['loginemail'], 'email');
|
||||
$result_stmt = Database::prepare("SELECT `adminid`, `customerid`, `firstname`, `name`, `company`, `email`, `loginname`, `def_language`, `deactivated` FROM `" . TABLE_PANEL_CUSTOMERS . "`
|
||||
WHERE `loginname`= :loginname
|
||||
AND `email`= :email"
|
||||
);
|
||||
Database::pexecute($result_stmt, array("loginname" => $loginname, "email" => $email));
|
||||
AND `email`= :email");
|
||||
Database::pexecute($result_stmt, array(
|
||||
"loginname" => $loginname,
|
||||
"email" => $email
|
||||
));
|
||||
|
||||
if (Database::num_rows() == 0) {
|
||||
$result_stmt = Database::prepare("SELECT `adminid`, `name`, `email`, `loginname`, `def_language`, `deactivated` FROM `" . TABLE_PANEL_ADMINS . "`
|
||||
WHERE `loginname`= :loginname
|
||||
AND `email`= :email"
|
||||
);
|
||||
Database::pexecute($result_stmt, array("loginname" => $loginname, "email" => $email));
|
||||
AND `email`= :email");
|
||||
Database::pexecute($result_stmt, array(
|
||||
"loginname" => $loginname,
|
||||
"email" => $email
|
||||
));
|
||||
|
||||
if (Database::num_rows() > 0) {
|
||||
$adminchecked = true;
|
||||
@@ -341,8 +416,10 @@ if ($action == 'forgotpwd') {
|
||||
|
||||
/* Check whether user is banned */
|
||||
if ($user['deactivated']) {
|
||||
redirectTo('index.php', array('showmessage' => '8'));
|
||||
exit;
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '8'
|
||||
));
|
||||
exit();
|
||||
}
|
||||
|
||||
if (($adminchecked && Settings::Get('panel.allow_preset_admin') == '1') || $adminchecked == false) {
|
||||
@@ -356,8 +433,7 @@ if ($action == 'forgotpwd') {
|
||||
// Drop all existing activation codes for this user
|
||||
$stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_ACTIVATION . "`
|
||||
WHERE `userid` = :userid
|
||||
AND `admin` = :admin"
|
||||
);
|
||||
AND `admin` = :admin");
|
||||
$params = array(
|
||||
"userid" => $adminchecked ? $user['adminid'] : $user['customerid'],
|
||||
"admin" => $adminchecked ? 1 : 0
|
||||
@@ -367,8 +443,7 @@ if ($action == 'forgotpwd') {
|
||||
// Add new activation code to database
|
||||
$stmt = Database::prepare("INSERT INTO `" . TABLE_PANEL_ACTIVATION . "`
|
||||
(userid, admin, creation, activationcode)
|
||||
VALUES (:userid, :admin, :creation, :activationcode)"
|
||||
);
|
||||
VALUES (:userid, :admin, :creation, :activationcode)");
|
||||
$params = array(
|
||||
"userid" => $adminchecked ? $user['adminid'] : $user['customerid'],
|
||||
"admin" => $adminchecked ? 1 : 0,
|
||||
@@ -377,7 +452,9 @@ if ($action == 'forgotpwd') {
|
||||
);
|
||||
Database::pexecute($stmt, $params);
|
||||
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array('loginname' => 'password_reset'));
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => 'password_reset'
|
||||
));
|
||||
$rstlog->logAction(USR_ACTION, LOG_WARNING, "User '" . $user['loginname'] . "' requested a link for setting a new password.");
|
||||
|
||||
// Set together our activation link
|
||||
@@ -407,9 +484,11 @@ if ($action == 'forgotpwd') {
|
||||
WHERE `adminid`= :adminid
|
||||
AND `language`= :lang
|
||||
AND `templategroup`=\'mails\'
|
||||
AND `varname`=\'password_reset_subject\''
|
||||
);
|
||||
Database::pexecute($result_stmt, array("adminid" => $user['adminid'], "lang" => $def_language));
|
||||
AND `varname`=\'password_reset_subject\'');
|
||||
Database::pexecute($result_stmt, array(
|
||||
"adminid" => $user['adminid'],
|
||||
"lang" => $def_language
|
||||
));
|
||||
$result = $result_stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$mail_subject = html_entity_decode(replace_variables((($result['value'] != '') ? $result['value'] : $lng['mails']['password_reset']['subject']), $replace_arr));
|
||||
|
||||
@@ -417,13 +496,16 @@ if ($action == 'forgotpwd') {
|
||||
WHERE `adminid`= :adminid
|
||||
AND `language`= :lang
|
||||
AND `templategroup`=\'mails\'
|
||||
AND `varname`=\'password_reset_mailbody\''
|
||||
);
|
||||
Database::pexecute($result_stmt, array("adminid" => $user['adminid'], "lang" => $def_language));
|
||||
AND `varname`=\'password_reset_mailbody\'');
|
||||
Database::pexecute($result_stmt, array(
|
||||
"adminid" => $user['adminid'],
|
||||
"lang" => $def_language
|
||||
));
|
||||
$result = $result_stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$mail_body = html_entity_decode(replace_variables((($result['value'] != '') ? $result['value'] : $lng['mails']['password_reset']['mailbody']), $replace_arr));
|
||||
|
||||
$_mailerror = false;
|
||||
$mailerr_msg = "";
|
||||
try {
|
||||
$mail->Subject = $mail_subject;
|
||||
$mail->AltBody = $mail_body;
|
||||
@@ -439,17 +521,26 @@ if ($action == 'forgotpwd') {
|
||||
}
|
||||
|
||||
if ($_mailerror) {
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array('loginname' => 'password_reset'));
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => 'password_reset'
|
||||
));
|
||||
$rstlog->logAction(ADM_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg);
|
||||
redirectTo('index.php', array('showmessage' => '4', 'customermail' => $user['email']));
|
||||
exit;
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '4',
|
||||
'customermail' => $user['email']
|
||||
));
|
||||
exit();
|
||||
}
|
||||
|
||||
$mail->ClearAddresses();
|
||||
redirectTo('index.php', array('showmessage' => '1'));
|
||||
exit;
|
||||
redirectTo('index.php', array(
|
||||
'showmessage' => '1'
|
||||
));
|
||||
exit();
|
||||
} else {
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array('loginname' => 'password_reset'));
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => 'password_reset'
|
||||
));
|
||||
$rstlog->logAction(USR_ACTION, LOG_WARNING, "User '" . $loginname . "' requested to set a new password, but was not found in database!");
|
||||
$message = $lng['login']['combination_not_found'];
|
||||
}
|
||||
@@ -480,9 +571,10 @@ if ($action == 'resetpwd') {
|
||||
|
||||
// Remove old activation codes
|
||||
$stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_ACTIVATION . "`
|
||||
WHERE creation < :oldest"
|
||||
);
|
||||
Database::pexecute($stmt, array("oldest" => time() - 86400));
|
||||
WHERE creation < :oldest");
|
||||
Database::pexecute($stmt, array(
|
||||
"oldest" => time() - 86400
|
||||
));
|
||||
|
||||
if (isset($_GET['resetcode']) && strlen($_GET['resetcode']) == 50) {
|
||||
// Check if activation code is valid
|
||||
@@ -494,9 +586,10 @@ if ($action == 'resetpwd') {
|
||||
if (substr(md5($third . $timestamp), 0, 10) == $check && $timestamp >= time() - 86400) {
|
||||
if (isset($_POST['send']) && $_POST['send'] == 'send') {
|
||||
$stmt = Database::prepare("SELECT `userid`, `admin` FROM `" . TABLE_PANEL_ACTIVATION . "`
|
||||
WHERE `activationcode` = :activationcode"
|
||||
);
|
||||
$result = Database::pexecute_first($stmt, array("activationcode" => $activationcode));
|
||||
WHERE `activationcode` = :activationcode");
|
||||
$result = Database::pexecute_first($stmt, array(
|
||||
"activationcode" => $activationcode
|
||||
));
|
||||
|
||||
if ($result !== false) {
|
||||
if ($result['admin'] == 1) {
|
||||
@@ -518,39 +611,148 @@ if ($action == 'resetpwd') {
|
||||
if ($result['admin'] == 1) {
|
||||
$stmt = Database::prepare("UPDATE `" . TABLE_PANEL_ADMINS . "`
|
||||
SET `password` = :newpassword
|
||||
WHERE `adminid` = :userid"
|
||||
);
|
||||
WHERE `adminid` = :userid");
|
||||
} else {
|
||||
$stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "`
|
||||
SET `password` = :newpassword
|
||||
WHERE `customerid` = :userid"
|
||||
);
|
||||
WHERE `customerid` = :userid");
|
||||
}
|
||||
Database::pexecute($stmt, array("newpassword" => makeCryptPassword($new_password), "userid" => $result['userid']));
|
||||
Database::pexecute($stmt, array(
|
||||
"newpassword" => makeCryptPassword($new_password),
|
||||
"userid" => $result['userid']
|
||||
));
|
||||
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array('loginname' => 'password_reset'));
|
||||
$rstlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => 'password_reset'
|
||||
));
|
||||
$rstlog->logAction(USR_ACTION, LOG_NOTICE, "changed password using password reset.");
|
||||
|
||||
// Remove activation code from DB
|
||||
$stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_ACTIVATION . "`
|
||||
WHERE `activationcode` = :activationcode
|
||||
AND `userid` = :userid"
|
||||
);
|
||||
Database::pexecute($stmt, array("activationcode" => $activationcode, "userid" => $result['userid']));
|
||||
redirectTo('index.php', array("showmessage" => '6'));
|
||||
AND `userid` = :userid");
|
||||
Database::pexecute($stmt, array(
|
||||
"activationcode" => $activationcode,
|
||||
"userid" => $result['userid']
|
||||
));
|
||||
redirectTo('index.php', array(
|
||||
"showmessage" => '6'
|
||||
));
|
||||
}
|
||||
} else {
|
||||
redirectTo('index.php', array("showmessage" => '7'));
|
||||
redirectTo('index.php', array(
|
||||
"showmessage" => '7'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
eval("echo \"" . getTemplate('rpwd') . "\";");
|
||||
|
||||
} else {
|
||||
redirectTo('index.php', array("showmessage" => '7'));
|
||||
redirectTo('index.php', array(
|
||||
"showmessage" => '7'
|
||||
));
|
||||
}
|
||||
|
||||
} else {
|
||||
redirectTo('index.php');
|
||||
}
|
||||
}
|
||||
|
||||
function finishLogin($userinfo)
|
||||
{
|
||||
global $version, $dbversion, $remote_addr, $http_user_agent, $languages;
|
||||
|
||||
if (isset($userinfo['userid']) && $userinfo['userid'] != '') {
|
||||
$s = md5(uniqid(microtime(), 1));
|
||||
|
||||
if (isset($_POST['language'])) {
|
||||
$language = validate($_POST['language'], 'language');
|
||||
if ($language == 'profile') {
|
||||
$language = $userinfo['def_language'];
|
||||
} elseif (! isset($languages[$language])) {
|
||||
$language = Settings::Get('panel.standardlanguage');
|
||||
}
|
||||
} else {
|
||||
$language = Settings::Get('panel.standardlanguage');
|
||||
}
|
||||
|
||||
if (isset($userinfo['theme']) && $userinfo['theme'] != '') {
|
||||
$theme = $userinfo['theme'];
|
||||
} else {
|
||||
$theme = Settings::Get('panel.default_theme');
|
||||
}
|
||||
|
||||
if (Settings::Get('session.allow_multiple_login') != '1') {
|
||||
$stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_SESSIONS . "`
|
||||
WHERE `userid` = :uid
|
||||
AND `adminsession` = :adminsession");
|
||||
Database::pexecute($stmt, array(
|
||||
"uid" => $userinfo['userid'],
|
||||
"adminsession" => $userinfo['adminsession']
|
||||
));
|
||||
}
|
||||
|
||||
// check for field 'theme' in session-table, refs #607
|
||||
// Changed with #1287 to new method
|
||||
$stmt = Database::query("SHOW COLUMNS FROM panel_sessions LIKE 'theme'");
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
if ($row['Field'] == "theme") {
|
||||
$has_theme = true;
|
||||
}
|
||||
}
|
||||
|
||||
$params = array(
|
||||
"hash" => $s,
|
||||
"userid" => $userinfo['userid'],
|
||||
"ipaddress" => $remote_addr,
|
||||
"useragent" => $http_user_agent,
|
||||
"lastactivity" => time(),
|
||||
"language" => $language,
|
||||
"adminsession" => $userinfo['adminsession']
|
||||
);
|
||||
|
||||
if ($has_theme) {
|
||||
$params["theme"] = $theme;
|
||||
$stmt = Database::prepare("INSERT INTO `" . TABLE_PANEL_SESSIONS . "`
|
||||
(`hash`, `userid`, `ipaddress`, `useragent`, `lastactivity`, `language`, `adminsession`, `theme`)
|
||||
VALUES (:hash, :userid, :ipaddress, :useragent, :lastactivity, :language, :adminsession, :theme)");
|
||||
} else {
|
||||
$stmt = Database::prepare("INSERT INTO `" . TABLE_PANEL_SESSIONS . "`
|
||||
(`hash`, `userid`, `ipaddress`, `useragent`, `lastactivity`, `language`, `adminsession`)
|
||||
VALUES (:hash, :userid, :ipaddress, :useragent, :lastactivity, :language, :adminsession)");
|
||||
}
|
||||
Database::pexecute($stmt, $params);
|
||||
|
||||
$qryparams = array();
|
||||
if (isset($_POST['qrystr']) && $_POST['qrystr'] != "") {
|
||||
parse_str(urldecode($_POST['qrystr']), $qryparams);
|
||||
}
|
||||
$qryparams['s'] = $s;
|
||||
|
||||
if ($userinfo['adminsession'] == '1') {
|
||||
if (hasUpdates($version) || hasDbUpdates($dbversion)) {
|
||||
redirectTo('admin_updates.php', array(
|
||||
's' => $s
|
||||
));
|
||||
} else {
|
||||
if (isset($_POST['script']) && $_POST['script'] != "") {
|
||||
if (preg_match("/customer\_/", $_POST['script']) === 1) {
|
||||
redirectTo('admin_customers.php', array(
|
||||
"page" => "customers"
|
||||
));
|
||||
} else {
|
||||
redirectTo($_POST['script'], $qryparams);
|
||||
}
|
||||
} else {
|
||||
redirectTo('admin_index.php', $qryparams);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isset($_POST['script']) && $_POST['script'] != "") {
|
||||
redirectTo($_POST['script'], $qryparams);
|
||||
} else {
|
||||
redirectTo('customer_index.php', $qryparams);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -133,6 +133,8 @@ CREATE TABLE `panel_admins` (
|
||||
`theme` varchar(255) NOT NULL default 'Sparkle',
|
||||
`custom_notes` text,
|
||||
`custom_notes_show` tinyint(1) NOT NULL default '0',
|
||||
`type_2fa` tinyint(1) NOT NULL default '0',
|
||||
`data_2fa` varchar(500) NOT NULL default '',
|
||||
PRIMARY KEY (`adminid`),
|
||||
UNIQUE KEY `loginname` (`loginname`)
|
||||
) ENGINE=MyISAM CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
@@ -200,6 +202,8 @@ CREATE TABLE `panel_customers` (
|
||||
`leregistered` tinyint(1) NOT NULL default '0',
|
||||
`leaccount` varchar(255) default '',
|
||||
`allowed_phpconfigs` varchar(500) NOT NULL default '',
|
||||
`type_2fa` tinyint(1) NOT NULL default '0',
|
||||
`data_2fa` varchar(500) NOT NULL default '',
|
||||
PRIMARY KEY (`customerid`),
|
||||
UNIQUE KEY `loginname` (`loginname`)
|
||||
) ENGINE=MyISAM CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
@@ -656,6 +660,7 @@ opcache.interned_strings_buffer'),
|
||||
('system', 'logfiles_script', ''),
|
||||
('system', 'dhparams_file', ''),
|
||||
('api', 'enabled', '0'),
|
||||
('2fa', 'enabled', '1'),
|
||||
('panel', 'decimal_places', '4'),
|
||||
('panel', 'adminmail', 'admin@SERVERNAME'),
|
||||
('panel', 'phpmyadmin_url', ''),
|
||||
@@ -688,7 +693,7 @@ opcache.interned_strings_buffer'),
|
||||
('panel', 'password_special_char', '!?<>§$%+#=@'),
|
||||
('panel', 'customer_hide_options', ''),
|
||||
('panel', 'version', '0.10.0'),
|
||||
('panel', 'db_version', '201811180');
|
||||
('panel', 'db_version', '201811300');
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `panel_tasks`;
|
||||
|
||||
@@ -77,3 +77,22 @@ if (isDatabaseVersion('201809280')) {
|
||||
|
||||
updateToDbVersion('201811180');
|
||||
}
|
||||
|
||||
if (isDatabaseVersion('201811180')) {
|
||||
|
||||
showUpdateStep("Adding new settings for 2FA");
|
||||
Settings::Add('2fa.enabled', '1', true);
|
||||
lastStepStatus(0);
|
||||
|
||||
showUpdateStep("Adding new fields to admin-table for 2FA");
|
||||
Database::query("ALTER TABLE `" . TABLE_PANEL_ADMINS . "` ADD `type_2fa` tinyint(1) NOT NULL default '0';");
|
||||
Database::query("ALTER TABLE `" . TABLE_PANEL_ADMINS . "` ADD `data_2fa` varchar(500) NOT NULL default '' AFTER `type_2fa`;");
|
||||
lastStepStatus(0);
|
||||
|
||||
showUpdateStep("Adding new fields to customer-table for 2FA");
|
||||
Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` ADD `type_2fa` tinyint(1) NOT NULL default '0';");
|
||||
Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` ADD `data_2fa` varchar(500) NOT NULL default '' AFTER `type_2fa`;");
|
||||
lastStepStatus(0);
|
||||
|
||||
updateToDbVersion('201811300');
|
||||
}
|
||||
|
||||
189
lib/classes/2FA/.gitignore
vendored
Normal file
189
lib/classes/2FA/.gitignore
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
build/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
# Roslyn cache directories
|
||||
*.ide/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
#NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding addin-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# If using the old MSBuild-Integrated Package Restore, uncomment this:
|
||||
#!**/packages/repositories.config
|
||||
|
||||
# Windows Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Windows Store app package directory
|
||||
AppPackages/
|
||||
|
||||
# Others
|
||||
sql/
|
||||
*.Cache
|
||||
ClientBin/
|
||||
[Ss]tyle[Cc]op.*
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# Composer
|
||||
/vendor
|
||||
|
||||
# .vs
|
||||
.vs/
|
||||
38
lib/classes/2FA/class.FroxlorTwoFactorAuth.php
Normal file
38
lib/classes/2FA/class.FroxlorTwoFactorAuth.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Froxlor project.
|
||||
* Copyright (c) 2010 the Froxlor Team (see authors).
|
||||
*
|
||||
* For the full copyright and license information, please view the COPYING
|
||||
* file that was distributed with this source code. You can also view the
|
||||
* COPYING file online at http://files.froxlor.org/misc/COPYING.txt
|
||||
*
|
||||
* @copyright (c) the authors
|
||||
* @author Froxlor team <team@froxlor.org> (2010-)
|
||||
* @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
|
||||
* @package API
|
||||
* @since 0.10.0
|
||||
*
|
||||
*/
|
||||
require_once __DIR__ . '/lib/TwoFactorAuthException.php';
|
||||
require_once __DIR__ . '/lib/Providers/Rng/RNGException.php';
|
||||
require_once __DIR__ . '/lib/Providers/Rng/IRNGProvider.php';
|
||||
require_once __DIR__ . '/lib/Providers/Rng/CSRNGProvider.php';
|
||||
require_once __DIR__ . '/lib/Providers/Rng/HashRNGProvider.php';
|
||||
require_once __DIR__ . '/lib/Providers/Rng/MCryptRNGProvider.php';
|
||||
require_once __DIR__ . '/lib/Providers/Rng/OpenSSLRNGProvider.php';
|
||||
require_once __DIR__ . '/lib/Providers/Qr/QRException.php';
|
||||
require_once __DIR__ . '/lib/Providers/Qr/IQRCodeProvider.php';
|
||||
require_once __DIR__ . '/lib/Providers/Qr/BaseHTTPQRCodeProvider.php';
|
||||
require_once __DIR__ . '/lib/Providers/Qr/GoogleQRCodeProvider.php';
|
||||
require_once __DIR__ . '/lib/Providers/Time/TimeException.php';
|
||||
require_once __DIR__ . '/lib/Providers/Time/ITimeProvider.php';
|
||||
require_once __DIR__ . '/lib/Providers/Time/LocalMachineTimeProvider.php';
|
||||
require_once __DIR__ . '/lib/Providers/Time/HttpTimeProvider.php';
|
||||
require_once __DIR__ . '/lib/Providers/Time/NTPTimeProvider.php';
|
||||
require_once __DIR__ . '/lib/TwoFactorAuth.php';
|
||||
|
||||
class FroxlorTwoFactorAuth extends \RobThree\Auth\TwoFactorAuth
|
||||
{
|
||||
}
|
||||
27
lib/classes/2FA/lib/Providers/Qr/BaseHTTPQRCodeProvider.php
Normal file
27
lib/classes/2FA/lib/Providers/Qr/BaseHTTPQRCodeProvider.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
abstract class BaseHTTPQRCodeProvider implements IQRCodeProvider
|
||||
{
|
||||
protected $verifyssl;
|
||||
|
||||
protected function getContent($url)
|
||||
{
|
||||
$curlhandle = curl_init();
|
||||
|
||||
curl_setopt_array($curlhandle, array(
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_DNS_CACHE_TIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_SSL_VERIFYPEER => $this->verifyssl,
|
||||
CURLOPT_USERAGENT => 'TwoFactorAuth'
|
||||
));
|
||||
$data = curl_exec($curlhandle);
|
||||
|
||||
curl_close($curlhandle);
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
39
lib/classes/2FA/lib/Providers/Qr/GoogleQRCodeProvider.php
Normal file
39
lib/classes/2FA/lib/Providers/Qr/GoogleQRCodeProvider.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
// https://developers.google.com/chart/infographics/docs/qr_codes
|
||||
class GoogleQRCodeProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
public $errorcorrectionlevel;
|
||||
public $margin;
|
||||
|
||||
function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 1)
|
||||
{
|
||||
if (!is_bool($verifyssl))
|
||||
throw new \QRException('VerifySSL must be bool');
|
||||
|
||||
$this->verifyssl = $verifyssl;
|
||||
|
||||
$this->errorcorrectionlevel = $errorcorrectionlevel;
|
||||
$this->margin = $margin;
|
||||
}
|
||||
|
||||
public function getMimeType()
|
||||
{
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
}
|
||||
|
||||
public function getUrl($qrtext, $size)
|
||||
{
|
||||
return 'https://www.google.com/chart?cht=qr'
|
||||
. '&chs=' . $size . 'x' . $size
|
||||
. '&chld=' . $this->errorcorrectionlevel . '|' . $this->margin
|
||||
. '&chl=' . rawurlencode($qrtext);
|
||||
}
|
||||
}
|
||||
9
lib/classes/2FA/lib/Providers/Qr/IQRCodeProvider.php
Normal file
9
lib/classes/2FA/lib/Providers/Qr/IQRCodeProvider.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
interface IQRCodeProvider
|
||||
{
|
||||
public function getQRCodeImage($qrtext, $size);
|
||||
public function getMimeType();
|
||||
}
|
||||
5
lib/classes/2FA/lib/Providers/Qr/QRException.php
Normal file
5
lib/classes/2FA/lib/Providers/Qr/QRException.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class QRException extends TwoFactorAuthException {}
|
||||
71
lib/classes/2FA/lib/Providers/Qr/QRServerProvider.php
Normal file
71
lib/classes/2FA/lib/Providers/Qr/QRServerProvider.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
// http://goqr.me/api/doc/create-qr-code/
|
||||
class QRServerProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
public $errorcorrectionlevel;
|
||||
public $margin;
|
||||
public $qzone;
|
||||
public $bgcolor;
|
||||
public $color;
|
||||
public $format;
|
||||
|
||||
function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 4, $qzone = 1, $bgcolor = 'ffffff', $color = '000000', $format = 'png')
|
||||
{
|
||||
if (!is_bool($verifyssl))
|
||||
throw new QRException('VerifySSL must be bool');
|
||||
|
||||
$this->verifyssl = $verifyssl;
|
||||
|
||||
$this->errorcorrectionlevel = $errorcorrectionlevel;
|
||||
$this->margin = $margin;
|
||||
$this->qzone = $qzone;
|
||||
$this->bgcolor = $bgcolor;
|
||||
$this->color = $color;
|
||||
$this->format = $format;
|
||||
}
|
||||
|
||||
public function getMimeType()
|
||||
{
|
||||
switch (strtolower($this->format))
|
||||
{
|
||||
case 'png':
|
||||
return 'image/png';
|
||||
case 'gif':
|
||||
return 'image/gif';
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
return 'image/jpeg';
|
||||
case 'svg':
|
||||
return 'image/svg+xml';
|
||||
case 'eps':
|
||||
return 'application/postscript';
|
||||
}
|
||||
throw new \QRException(sprintf('Unknown MIME-type: %s', $this->format));
|
||||
}
|
||||
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
}
|
||||
|
||||
private function decodeColor($value)
|
||||
{
|
||||
return vsprintf('%d-%d-%d', sscanf($value, "%02x%02x%02x"));
|
||||
}
|
||||
|
||||
public function getUrl($qrtext, $size)
|
||||
{
|
||||
return 'https://api.qrserver.com/v1/create-qr-code/'
|
||||
. '?size=' . $size . 'x' . $size
|
||||
. '&ecc=' . strtoupper($this->errorcorrectionlevel)
|
||||
. '&margin=' . $this->margin
|
||||
. '&qzone=' . $this->qzone
|
||||
. '&bgcolor=' . $this->decodeColor($this->bgcolor)
|
||||
. '&color=' . $this->decodeColor($this->color)
|
||||
. '&format=' . strtolower($this->format)
|
||||
. '&data=' . rawurlencode($qrtext);
|
||||
}
|
||||
}
|
||||
54
lib/classes/2FA/lib/Providers/Qr/QRicketProvider.php
Normal file
54
lib/classes/2FA/lib/Providers/Qr/QRicketProvider.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
// http://qrickit.com/qrickit_apps/qrickit_api.php
|
||||
class QRicketProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
public $errorcorrectionlevel;
|
||||
public $margin;
|
||||
public $qzone;
|
||||
public $bgcolor;
|
||||
public $color;
|
||||
public $format;
|
||||
|
||||
function __construct($errorcorrectionlevel = 'L', $bgcolor = 'ffffff', $color = '000000', $format = 'p')
|
||||
{
|
||||
$this->verifyssl = false;
|
||||
|
||||
$this->errorcorrectionlevel = $errorcorrectionlevel;
|
||||
$this->bgcolor = $bgcolor;
|
||||
$this->color = $color;
|
||||
$this->format = $format;
|
||||
}
|
||||
|
||||
public function getMimeType()
|
||||
{
|
||||
switch (strtolower($this->format))
|
||||
{
|
||||
case 'p':
|
||||
return 'image/png';
|
||||
case 'g':
|
||||
return 'image/gif';
|
||||
case 'j':
|
||||
return 'image/jpeg';
|
||||
}
|
||||
throw new \QRException(sprintf('Unknown MIME-type: %s', $this->format));
|
||||
}
|
||||
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
}
|
||||
|
||||
public function getUrl($qrtext, $size)
|
||||
{
|
||||
return 'http://qrickit.com/api/qr'
|
||||
. '?qrsize=' . $size
|
||||
. '&e=' . strtolower($this->errorcorrectionlevel)
|
||||
. '&bgdcolor=' . $this->bgcolor
|
||||
. '&fgdcolor=' . $this->color
|
||||
. '&t=' . strtolower($this->format)
|
||||
. '&d=' . rawurlencode($qrtext);
|
||||
}
|
||||
}
|
||||
14
lib/classes/2FA/lib/Providers/Rng/CSRNGProvider.php
Normal file
14
lib/classes/2FA/lib/Providers/Rng/CSRNGProvider.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class CSRNGProvider implements IRNGProvider
|
||||
{
|
||||
public function getRandomBytes($bytecount) {
|
||||
return random_bytes($bytecount); // PHP7+
|
||||
}
|
||||
|
||||
public function isCryptographicallySecure() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
28
lib/classes/2FA/lib/Providers/Rng/HashRNGProvider.php
Normal file
28
lib/classes/2FA/lib/Providers/Rng/HashRNGProvider.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class HashRNGProvider implements IRNGProvider
|
||||
{
|
||||
private $algorithm;
|
||||
|
||||
function __construct($algorithm = 'sha256' ) {
|
||||
$algos = array_values(hash_algos());
|
||||
if (!in_array($algorithm, $algos, true))
|
||||
throw new \RNGException('Unsupported algorithm specified');
|
||||
$this->algorithm = $algorithm;
|
||||
}
|
||||
|
||||
public function getRandomBytes($bytecount) {
|
||||
$result = '';
|
||||
$hash = mt_rand();
|
||||
for ($i = 0; $i < $bytecount; $i++) {
|
||||
$hash = hash($this->algorithm, $hash.mt_rand(), true);
|
||||
$result .= $hash[mt_rand(0, strlen($hash)-1)];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function isCryptographicallySecure() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
9
lib/classes/2FA/lib/Providers/Rng/IRNGProvider.php
Normal file
9
lib/classes/2FA/lib/Providers/Rng/IRNGProvider.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
interface IRNGProvider
|
||||
{
|
||||
public function getRandomBytes($bytecount);
|
||||
public function isCryptographicallySecure();
|
||||
}
|
||||
23
lib/classes/2FA/lib/Providers/Rng/MCryptRNGProvider.php
Normal file
23
lib/classes/2FA/lib/Providers/Rng/MCryptRNGProvider.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class MCryptRNGProvider implements IRNGProvider
|
||||
{
|
||||
private $source;
|
||||
|
||||
function __construct($source = MCRYPT_DEV_URANDOM) {
|
||||
$this->source = $source;
|
||||
}
|
||||
|
||||
public function getRandomBytes($bytecount) {
|
||||
$result = @mcrypt_create_iv($bytecount, $this->source);
|
||||
if ($result === false)
|
||||
throw new \RNGException('mcrypt_create_iv returned an invalid value');
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function isCryptographicallySecure() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
25
lib/classes/2FA/lib/Providers/Rng/OpenSSLRNGProvider.php
Normal file
25
lib/classes/2FA/lib/Providers/Rng/OpenSSLRNGProvider.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class OpenSSLRNGProvider implements IRNGProvider
|
||||
{
|
||||
private $requirestrong;
|
||||
|
||||
function __construct($requirestrong = true) {
|
||||
$this->requirestrong = $requirestrong;
|
||||
}
|
||||
|
||||
public function getRandomBytes($bytecount) {
|
||||
$result = openssl_random_pseudo_bytes($bytecount, $crypto_strong);
|
||||
if ($this->requirestrong && ($crypto_strong === false))
|
||||
throw new \RNGException('openssl_random_pseudo_bytes returned non-cryptographically strong value');
|
||||
if ($result === false)
|
||||
throw new \RNGException('openssl_random_pseudo_bytes returned an invalid value');
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function isCryptographicallySecure() {
|
||||
return $this->requirestrong;
|
||||
}
|
||||
}
|
||||
5
lib/classes/2FA/lib/Providers/Rng/RNGException.php
Normal file
5
lib/classes/2FA/lib/Providers/Rng/RNGException.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class RNGException extends TwoFactorAuthException {}
|
||||
54
lib/classes/2FA/lib/Providers/Time/HttpTimeProvider.php
Normal file
54
lib/classes/2FA/lib/Providers/Time/HttpTimeProvider.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
/**
|
||||
* Takes the time from any webserver by doing a HEAD request on the specified URL and extracting the 'Date:' header
|
||||
*/
|
||||
class HttpTimeProvider implements ITimeProvider
|
||||
{
|
||||
public $url;
|
||||
public $options;
|
||||
public $expectedtimeformat;
|
||||
|
||||
function __construct($url = 'https://google.com', $expectedtimeformat = 'D, d M Y H:i:s O+', array $options = null)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->expectedtimeformat = $expectedtimeformat;
|
||||
$this->options = $options;
|
||||
if ($this->options === null) {
|
||||
$this->options = array(
|
||||
'http' => array(
|
||||
'method' => 'HEAD',
|
||||
'follow_location' => false,
|
||||
'ignore_errors' => true,
|
||||
'max_redirects' => 0,
|
||||
'request_fulluri' => true,
|
||||
'header' => array(
|
||||
'Connection: close',
|
||||
'User-agent: TwoFactorAuth HttpTimeProvider (https://github.com/RobThree/TwoFactorAuth)',
|
||||
'Cache-Control: no-cache'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getTime() {
|
||||
try {
|
||||
$context = stream_context_create($this->options);
|
||||
$fd = fopen($this->url, 'rb', false, $context);
|
||||
$headers = stream_get_meta_data($fd);
|
||||
fclose($fd);
|
||||
|
||||
foreach ($headers['wrapper_data'] as $h) {
|
||||
if (strcasecmp(substr($h, 0, 5), 'Date:') === 0)
|
||||
return \DateTime::createFromFormat($this->expectedtimeformat, trim(substr($h,5)))->getTimestamp();
|
||||
}
|
||||
throw new \TimeException(sprintf('Unable to retrieve time from %s (Invalid or no "Date:" header found)', $this->url));
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
throw new \TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->url, $ex->getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
8
lib/classes/2FA/lib/Providers/Time/ITimeProvider.php
Normal file
8
lib/classes/2FA/lib/Providers/Time/ITimeProvider.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
interface ITimeProvider
|
||||
{
|
||||
public function getTime();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
class LocalMachineTimeProvider implements ITimeProvider {
|
||||
public function getTime() {
|
||||
return time();
|
||||
}
|
||||
}
|
||||
52
lib/classes/2FA/lib/Providers/Time/NTPTimeProvider.php
Normal file
52
lib/classes/2FA/lib/Providers/Time/NTPTimeProvider.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
/**
|
||||
* Takes the time from any NTP server
|
||||
*/
|
||||
class NTPTimeProvider implements ITimeProvider
|
||||
{
|
||||
public $host;
|
||||
public $port;
|
||||
public $timeout;
|
||||
|
||||
function __construct($host = 'pool.ntp.org', $port = 123, $timeout = 1)
|
||||
{
|
||||
$this->host = $host;
|
||||
|
||||
if (!is_int($port) || $port <= 0 || $port > 65535)
|
||||
throw new \TimeException('Port must be 0 < port < 65535');
|
||||
$this->port = $port;
|
||||
|
||||
if (!is_int($timeout) || $timeout < 0)
|
||||
throw new \TimeException('Timeout must be >= 0');
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
public function getTime() {
|
||||
try {
|
||||
/* Create a socket and connect to NTP server */
|
||||
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
|
||||
socket_connect($sock, $this->host, $this->port);
|
||||
|
||||
/* Send request */
|
||||
$msg = "\010" . str_repeat("\0", 47);
|
||||
socket_send($sock, $msg, strlen($msg), 0);
|
||||
|
||||
/* Receive response and close socket */
|
||||
socket_recv($sock, $recv, 48, MSG_WAITALL);
|
||||
socket_close($sock);
|
||||
|
||||
/* Interpret response */
|
||||
$data = unpack('N12', $recv);
|
||||
$timestamp = sprintf('%u', $data[9]);
|
||||
|
||||
/* NTP is number of seconds since 0000 UT on 1 January 1900 Unix time is seconds since 0000 UT on 1 January 1970 */
|
||||
return $timestamp - 2208988800;
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
throw new \TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->host, $ex->getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
5
lib/classes/2FA/lib/Providers/Time/TimeException.php
Normal file
5
lib/classes/2FA/lib/Providers/Time/TimeException.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class TimeException extends TwoFactorAuthException {}
|
||||
256
lib/classes/2FA/lib/TwoFactorAuth.php
Normal file
256
lib/classes/2FA/lib/TwoFactorAuth.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
namespace RobThree\Auth;
|
||||
|
||||
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
|
||||
use RobThree\Auth\Providers\Rng\IRNGProvider;
|
||||
use RobThree\Auth\Providers\Time\ITimeProvider;
|
||||
|
||||
// Based on / inspired by: https://github.com/PHPGangsta/GoogleAuthenticator
|
||||
// Algorithms, digits, period etc. explained: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
class TwoFactorAuth
|
||||
{
|
||||
private $algorithm;
|
||||
private $period;
|
||||
private $digits;
|
||||
private $issuer;
|
||||
private $qrcodeprovider = null;
|
||||
private $rngprovider = null;
|
||||
private $timeprovider = null;
|
||||
private static $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
|
||||
private static $_base32;
|
||||
private static $_base32lookup = array();
|
||||
private static $_supportedalgos = array('sha1', 'sha256', 'sha512', 'md5');
|
||||
|
||||
function __construct($issuer = null, $digits = 6, $period = 30, $algorithm = 'sha1', IQRCodeProvider $qrcodeprovider = null, IRNGProvider $rngprovider = null, ITimeProvider $timeprovider = null)
|
||||
{
|
||||
$this->issuer = $issuer;
|
||||
if (!is_int($digits) || $digits <= 0)
|
||||
throw new TwoFactorAuthException('Digits must be int > 0');
|
||||
$this->digits = $digits;
|
||||
|
||||
if (!is_int($period) || $period <= 0)
|
||||
throw new TwoFactorAuthException('Period must be int > 0');
|
||||
$this->period = $period;
|
||||
|
||||
$algorithm = strtolower(trim($algorithm));
|
||||
if (!in_array($algorithm, self::$_supportedalgos))
|
||||
throw new TwoFactorAuthException('Unsupported algorithm: ' . $algorithm);
|
||||
$this->algorithm = $algorithm;
|
||||
$this->qrcodeprovider = $qrcodeprovider;
|
||||
$this->rngprovider = $rngprovider;
|
||||
$this->timeprovider = $timeprovider;
|
||||
|
||||
self::$_base32 = str_split(self::$_base32dict);
|
||||
self::$_base32lookup = array_flip(self::$_base32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new secret
|
||||
*/
|
||||
public function createSecret($bits = 80, $requirecryptosecure = true)
|
||||
{
|
||||
$secret = '';
|
||||
$bytes = ceil($bits / 5); //We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32)
|
||||
$rngprovider = $this->getRngprovider();
|
||||
if ($requirecryptosecure && !$rngprovider->isCryptographicallySecure())
|
||||
throw new TwoFactorAuthException('RNG provider is not cryptographically secure');
|
||||
$rnd = $rngprovider->getRandomBytes($bytes);
|
||||
for ($i = 0; $i < $bytes; $i++)
|
||||
$secret .= self::$_base32[ord($rnd[$i]) & 31]; //Mask out left 3 bits for 0-31 values
|
||||
return $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the code with given secret and point in time
|
||||
*/
|
||||
public function getCode($secret, $time = null)
|
||||
{
|
||||
$secretkey = $this->base32Decode($secret);
|
||||
|
||||
$timestamp = chr(0).chr(0).chr(0).chr(0) . pack('N*', $this->getTimeSlice($this->getTime($time))); // Pack time into binary string
|
||||
$hashhmac = hash_hmac($this->algorithm, $timestamp, $secretkey, true); // Hash it with users secret key
|
||||
$hashpart = substr($hashhmac, ord(substr($hashhmac, -1)) & 0x0F, 4); // Use last nibble of result as index/offset and grab 4 bytes of the result
|
||||
$value = unpack('N', $hashpart); // Unpack binary value
|
||||
$value = $value[1] & 0x7FFFFFFF; // Drop MSB, keep only 31 bits
|
||||
|
||||
return str_pad($value % pow(10, $this->digits), $this->digits, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the code is correct. This will accept codes starting from ($discrepancy * $period) sec ago to ($discrepancy * period) sec from now
|
||||
*/
|
||||
public function verifyCode($secret, $code, $discrepancy = 1, $time = null, &$timeslice = 0)
|
||||
{
|
||||
$timetamp = $this->getTime($time);
|
||||
|
||||
$timeslice = 0;
|
||||
|
||||
// To keep safe from timing-attacks we iterate *all* possible codes even though we already may have
|
||||
// verified a code is correct. We use the timeslice variable to hold either 0 (no match) or the timeslice
|
||||
// of the match. Each iteration we either set the timeslice variable to the timeslice of the match
|
||||
// or set the value to itself. This is an effort to maintain constant execution time for the code.
|
||||
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
|
||||
$ts = $timetamp + ($i * $this->period);
|
||||
$slice = $this->getTimeSlice($ts);
|
||||
$timeslice = $this->codeEquals($this->getCode($secret, $ts), $code) ? $slice : $timeslice;
|
||||
}
|
||||
|
||||
return $timeslice > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timing-attack safe comparison of 2 codes (see http://blog.ircmaxell.com/2014/11/its-all-about-time.html)
|
||||
*/
|
||||
private function codeEquals($safe, $user) {
|
||||
if (function_exists('hash_equals')) {
|
||||
return hash_equals($safe, $user);
|
||||
}
|
||||
// In general, it's not possible to prevent length leaks. So it's OK to leak the length. The important part is that
|
||||
// we don't leak information about the difference of the two strings.
|
||||
if (strlen($safe)===strlen($user)) {
|
||||
$result = 0;
|
||||
for ($i = 0; $i < strlen($safe); $i++)
|
||||
$result |= (ord($safe[$i]) ^ ord($user[$i]));
|
||||
return $result === 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data-uri of QRCode
|
||||
*/
|
||||
public function getQRCodeImageAsDataUri($label, $secret, $size = 200)
|
||||
{
|
||||
if (!is_int($size) || $size <= 0)
|
||||
throw new TwoFactorAuthException('Size must be int > 0');
|
||||
|
||||
$qrcodeprovider = $this->getQrCodeProvider();
|
||||
return 'data:'
|
||||
. $qrcodeprovider->getMimeType()
|
||||
. ';base64,'
|
||||
. base64_encode($qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare default timeprovider with specified timeproviders and ensure the time is within the specified number of seconds (leniency)
|
||||
*/
|
||||
public function ensureCorrectTime(array $timeproviders = null, $leniency = 5)
|
||||
{
|
||||
if ($timeproviders != null && !is_array($timeproviders))
|
||||
throw new TwoFactorAuthException('No timeproviders specified');
|
||||
|
||||
if ($timeproviders == null)
|
||||
$timeproviders = array(
|
||||
new Providers\Time\NTPTimeProvider(),
|
||||
new Providers\Time\HttpTimeProvider()
|
||||
);
|
||||
|
||||
// Get default time provider
|
||||
$timeprovider = $this->getTimeProvider();
|
||||
|
||||
// Iterate specified time providers
|
||||
foreach ($timeproviders as $t) {
|
||||
if (!($t instanceof ITimeProvider))
|
||||
throw new TwoFactorAuthException('Object does not implement ITimeProvider');
|
||||
|
||||
// Get time from default time provider and compare to specific time provider and throw if time difference is more than specified number of seconds leniency
|
||||
if (abs($timeprovider->getTime() - $t->getTime()) > $leniency)
|
||||
throw new TwoFactorAuthException(sprintf('Time for timeprovider is off by more than %d seconds when compared to %s', $leniency, get_class($t)));
|
||||
}
|
||||
}
|
||||
|
||||
private function getTime($time)
|
||||
{
|
||||
return ($time === null) ? $this->getTimeProvider()->getTime() : $time;
|
||||
}
|
||||
|
||||
private function getTimeSlice($time = null, $offset = 0)
|
||||
{
|
||||
return (int)floor($time / $this->period) + ($offset * $this->period);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a string to be encoded in a QR code
|
||||
*/
|
||||
public function getQRText($label, $secret)
|
||||
{
|
||||
return 'otpauth://totp/' . rawurlencode($label)
|
||||
. '?secret=' . rawurlencode($secret)
|
||||
. '&issuer=' . rawurlencode($this->issuer)
|
||||
. '&period=' . intval($this->period)
|
||||
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm))
|
||||
. '&digits=' . intval($this->digits);
|
||||
}
|
||||
|
||||
private function base32Decode($value)
|
||||
{
|
||||
if (strlen($value)==0) return '';
|
||||
|
||||
if (preg_match('/[^'.preg_quote(self::$_base32dict).']/', $value) !== 0)
|
||||
throw new TwoFactorAuthException('Invalid base32 string');
|
||||
|
||||
$buffer = '';
|
||||
foreach (str_split($value) as $char)
|
||||
{
|
||||
if ($char !== '=')
|
||||
$buffer .= str_pad(decbin(self::$_base32lookup[$char]), 5, 0, STR_PAD_LEFT);
|
||||
}
|
||||
$length = strlen($buffer);
|
||||
$blocks = trim(chunk_split(substr($buffer, 0, $length - ($length % 8)), 8, ' '));
|
||||
|
||||
$output = '';
|
||||
foreach (explode(' ', $blocks) as $block)
|
||||
$output .= chr(bindec(str_pad($block, 8, 0, STR_PAD_RIGHT)));
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IQRCodeProvider
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function getQrCodeProvider()
|
||||
{
|
||||
// Set default QR Code provider if none was specified
|
||||
if (null === $this->qrcodeprovider) {
|
||||
return $this->qrcodeprovider = new Providers\Qr\GoogleQRCodeProvider();
|
||||
}
|
||||
return $this->qrcodeprovider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IRNGProvider
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function getRngprovider()
|
||||
{
|
||||
if (null !== $this->rngprovider) {
|
||||
return $this->rngprovider;
|
||||
}
|
||||
if (function_exists('random_bytes')) {
|
||||
return $this->rngprovider = new Providers\Rng\CSRNGProvider();
|
||||
}
|
||||
if (function_exists('mcrypt_create_iv')) {
|
||||
return $this->rngprovider = new Providers\Rng\MCryptRNGProvider();
|
||||
}
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
return $this->rngprovider = new Providers\Rng\OpenSSLRNGProvider();
|
||||
}
|
||||
if (function_exists('hash')) {
|
||||
return $this->rngprovider = new Providers\Rng\HashRNGProvider();
|
||||
}
|
||||
throw new TwoFactorAuthException('Unable to find a suited RNGProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ITimeProvider
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function getTimeProvider()
|
||||
{
|
||||
// Set default time provider if none was specified
|
||||
if (null === $this->timeprovider) {
|
||||
return $this->timeprovider = new Providers\Time\LocalMachineTimeProvider();
|
||||
}
|
||||
return $this->timeprovider;
|
||||
}
|
||||
}
|
||||
7
lib/classes/2FA/lib/TwoFactorAuthException.php
Normal file
7
lib/classes/2FA/lib/TwoFactorAuthException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth;
|
||||
|
||||
use Exception;
|
||||
|
||||
class TwoFactorAuthException extends \Exception {}
|
||||
@@ -19,7 +19,7 @@
|
||||
$version = '0.10.0';
|
||||
|
||||
// Database version (YYYYMMDDC where C is a daily counter)
|
||||
$dbversion = '201811180';
|
||||
$dbversion = '201811300';
|
||||
|
||||
// Distribution branding-tag (used for Debian etc.)
|
||||
$branding = '';
|
||||
|
||||
@@ -2151,3 +2151,17 @@ $lng['serversettings']['enable_api']['title'] = 'Enable external API usage';
|
||||
$lng['serversettings']['enable_api']['description'] = 'In order to use the froxlor API you need to activate this option. For more detailed information see <a href="https://api.froxlor.org/" target="_new">https://api.froxlor.org/</a>';
|
||||
$lng['serversettings']['dhparams_file']['title'] = 'DHParams file (Diffie–Hellman key exchange)';
|
||||
$lng['serversettings']['dhparams_file']['description'] = 'If a dhparams.pem file is specified here it will be included in the webserver configuration. Leave empty to disable.<br>Example: /etc/apache2/ssl/dhparams.pem<br><br>If the file does not exist, it will be created automatically with the following command: <em>openssl dhparam -out /etc/apache2/ssl/dhparams.pem 4096<em>. It is recommended to create the file prior to specifying it here as the creation takes quite a while and blocks the cronjob.';
|
||||
$lng['2fa']['2fa'] = '2FA options';
|
||||
$lng['2fa']['2fa_enabled'] = 'Activate Two-factor authentication (2FA)';
|
||||
$lng['login']['2fa'] = 'Two-factor authentication (2FA)';
|
||||
$lng['login']['2facode'] = 'Please enter 2FA code';
|
||||
$lng['2fa']['2fa_removed'] = '2FA removed successfully';
|
||||
$lng['2fa']['2fa_added'] = '2FA activated successfully<br><a href="'.$filename.'?s='.$s.'&page=2fa">View 2FA details</a>';
|
||||
$lng['2fa']['2fa_add'] = 'Activate 2FA';
|
||||
$lng['2fa']['2fa_delete'] = 'Deactivate 2FA';
|
||||
$lng['2fa']['2fa_verify'] = 'Verify code';
|
||||
$lng['mails']['2fa']['mailbody'] = 'Hello,\n\nyour 2FA login-code is: {CODE}.\n\nThis is an automatically created\ne-mail, please do not answer!\n\nYours sincerely, your administrator';
|
||||
$lng['mails']['2fa']['subject'] = 'Froxlor - 2FA Code';
|
||||
$lng['2fa']['2fa_overview_desc'] = 'Here you can activate a two-factor authentication for your account.<br><br>You can either use an authenticator-app (time-based one-time password / TOTP) or let froxlor send you an email to your account-address after each successful login with a one-time password.';
|
||||
$lng['2fa']['2fa_email_desc'] = 'Your account is set up to use one-time passwords via e-mail. To deactivate, click on "'.$lng['2fa']['2fa_delete'].'"';
|
||||
$lng['2fa']['2fa_ga_desc'] = 'Your account is set up to use time-based one-time passwords via authenticator-app. Please scan the QR code below with your desired authenticator app to generate the codes. To deactivate, click on "'.$lng['2fa']['2fa_delete'].'"';
|
||||
|
||||
@@ -1799,3 +1799,17 @@ $lng['serversettings']['enable_api']['title'] = 'Aktiviere externe API Nutzung';
|
||||
$lng['serversettings']['enable_api']['description'] = 'Um die froxlor API nutzen zu können, muss diese Option aktiviert sein. Für detaillierte Informationen siehe <a href="https://api.froxlor.org/" target="_new">https://api.froxlor.org/</a>';
|
||||
$lng['serversettings']['dhparams_file']['title'] = 'DHParams Datei (Diffie–Hellman key exchange)';
|
||||
$lng['serversettings']['dhparams_file']['description'] = 'Wird eine dhparams.pem Datei hier angegeben, wir sie in die Webserver Konfiguration mit eingefügt.<br>Beispiel: /etc/apache2/ssl/dhparams.pem<br><br>Existiert die Datei nicht, wird sie wie folgt erstellt: <em>openssl dhparam -out /etc/apache2/ssl/dhparams.pem 4096<em>. Es wird empfohlen die Datei zu erstellen, bevor sie hier angegeben wird, da die Erstellung längere Zeit in Anspruch nimmt und den Cronjob blockiert.';
|
||||
$lng['2fa']['2fa'] = '2FA Optionen';
|
||||
$lng['2fa']['2fa_enabled'] = 'Aktiviere Zwei-Faktor Authentifizierung (2FA)';
|
||||
$lng['login']['2fa'] = 'Zwei-Faktor Authentifizierung (2FA)';
|
||||
$lng['login']['2facode'] = 'Bitte 2FA Code angeben';
|
||||
$lng['2fa']['2fa_removed'] = '2FA erfolgreich gelöscht';
|
||||
$lng['2fa']['2fa_added'] = '2FA erfolgreich aktiviert<br><a href="'.$filename.'?s='.$s.'&page=2fa">2FA Details öffnen</a>';
|
||||
$lng['2fa']['2fa_add'] = '2FA aktivieren';
|
||||
$lng['2fa']['2fa_delete'] = '2FA deaktivieren';
|
||||
$lng['2fa']['2fa_verify'] = 'Code verifizieren';
|
||||
$lng['mails']['2fa']['mailbody'] = 'Hallo,\n\nihr 2FA-Login Code lautet: {CODE}\n\nDies ist eine automatisch generierte\neMail, bitte antworten Sie nicht auf\ndiese Mitteilung.\n\nIhr Administrator';
|
||||
$lng['mails']['2fa']['subject'] = 'Froxlor - 2FA Code';
|
||||
$lng['2fa']['2fa_overview_desc'] = 'Hier kann für das Konto eine Zwei-Faktor-Authentisierung aktiviert werden.<br><br>Es kann entweder eine Authenticator-App (time-based one-time password / TOTP) genutzt werden oder ein Einmalpasswort, welches nach erfolgreichem Login an die hinterlegte E-Mail Adresse gesendet wird.';
|
||||
$lng['2fa']['2fa_email_desc'] = 'Das Konto ist eingerichtet, um Einmalpasswörter per E-Mail zu erhalten. Zum Deaktivieren, klicke auf "'.$lng['2fa']['2fa_delete'].'"';
|
||||
$lng['2fa']['2fa_ga_desc'] = 'Das Konto ist eingerichtet, um zeitbasierte Einmalpasswörter via Authenticator-App zu erhalten. Um die gewünschte Authenticator-App einzurichten, scanne bitte den untenstehenden QR-Code. Zum Deaktivieren, klicke auf "'.$lng['2fa']['2fa_delete'].'"';
|
||||
|
||||
26
templates/Sparkle/2fa/entercode.tpl
vendored
Normal file
26
templates/Sparkle/2fa/entercode.tpl
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
$header
|
||||
<article class="login bradius">
|
||||
<header class="dark">
|
||||
<img src="{$header_logo}" alt="Froxlor Server Management Panel" />
|
||||
</header>
|
||||
<section class="loginsec">
|
||||
<form method="post" action="{$filename}" enctype="application/x-www-form-urlencoded">
|
||||
<fieldset>
|
||||
<legend>Froxlor - {$lng['login']['2fa']}</legend>
|
||||
<p>
|
||||
<label for="2fa_code">{$lng['login']['2facode']}:</label>
|
||||
<input type="text" name="2fa_code" id="2fa_code" value="" required/>
|
||||
</p>
|
||||
<p class="submit">
|
||||
<input type="hidden" name="action" value="2fa_verify" />
|
||||
<input type="hidden" name="send" value="send" />
|
||||
<input type="submit" value="{$lng['2fa']['2fa_verify']}" />
|
||||
</p>
|
||||
</fieldset>
|
||||
</form>
|
||||
<aside>
|
||||
<a href="index.php">{$lng['login']['backtologin']}</a>
|
||||
</aside>
|
||||
</section>
|
||||
</article>
|
||||
$footer
|
||||
40
templates/Sparkle/2fa/overview.tpl
vendored
Normal file
40
templates/Sparkle/2fa/overview.tpl
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
$header
|
||||
<article>
|
||||
<header>
|
||||
<h2>
|
||||
<img src="templates/{$theme}/assets/img/icons/lock_big.png"
|
||||
alt="" /> {$lng['login']['2fa']}
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<if $userinfo['type_2fa'] == '0'>
|
||||
<form method="post"
|
||||
action="{$linker->getLink(array('section' => 'index', 'page' => $page, 'action' => 'add'))}">
|
||||
<p>{$lng['2fa']['2fa_overview_desc']}</p><br>
|
||||
<select id="type_2fa" name="type_2fa" class="small">{$type_select}
|
||||
</select> <input type="submit" class="submit" value="{$lng['2fa']['2fa_add']}" name="add" />
|
||||
</form>
|
||||
</if>
|
||||
|
||||
<if $userinfo['type_2fa'] == '1'>
|
||||
<form method="post"
|
||||
action="{$linker->getLink(array('section' => 'index', 'page' => $page, 'action' => 'delete'))}">
|
||||
<p>{$lng['2fa']['2fa_email_desc']}</p><br>
|
||||
<input type="submit" class="cancel" value="{$lng['2fa']['2fa_delete']}" name="delete" />
|
||||
</form>
|
||||
</if>
|
||||
|
||||
<if $userinfo['type_2fa'] == '2'>
|
||||
<form method="post"
|
||||
action="{$linker->getLink(array('section' => 'index', 'page' => $page, 'action' => 'delete'))}">
|
||||
<p>{$lng['2fa']['2fa_ga_desc']}</p><br>
|
||||
<img src="{$ga_qrcode}" alt="QRCode" /><br><br>
|
||||
<input type="submit" class="cancel" value="{$lng['2fa']['2fa_delete']}" name="delete" />
|
||||
</form>
|
||||
</if>
|
||||
|
||||
</section>
|
||||
|
||||
</article>
|
||||
$footer
|
||||
3
templates/Sparkle/header.tpl
vendored
3
templates/Sparkle/header.tpl
vendored
@@ -61,6 +61,9 @@
|
||||
<ul>
|
||||
<li><a href="{$linker->getLink(array('section' => 'index', 'page' => 'change_password'))}">{$lng['login']['password']}</a></li>
|
||||
<li><a href="{$linker->getLink(array('section' => 'index', 'page' => 'change_language'))}">{$lng['login']['language']}</a></li>
|
||||
<if Settings::Get('2fa.enabled') == 1>
|
||||
<li><a href="{$linker->getLink(array('section' => 'index', 'page' => '2fa'))}">{$lng['2fa']['2fa']}</a></li>
|
||||
</if>
|
||||
<if Settings::Get('panel.allow_theme_change_admin') == '1' && $userinfo['adminsession'] == 1>
|
||||
<li><a href="{$linker->getLink(array('section' => 'index', 'page' => 'change_theme'))}">{$lng['panel']['theme']}</a></li>
|
||||
</if>
|
||||
|
||||
Reference in New Issue
Block a user