Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added TOTP MFA support per user. #756

Merged
merged 2 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/users/username.ini.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ encryption = clear

;Role
role = admin

;MFA Secret - This is generated inside of the admin area, set to "disabled" to turn off MFA for a user.
mfa_secret = disabled
2 changes: 2 additions & 0 deletions install.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,13 @@ protected function saveConfigs()
'encryption' => 'sha512',
'password' => hash('sha512', $this->userPassword),
'role' => 'admin',
'mfa_secret' => 'disabled',
), $userFile);
} else {
$userFile = $this->overwriteINI(array(
"password" => $this->userPassword,
'role' => 'admin',
'mfa_secret' => 'disabled',
), $userFile);
}
file_put_contents("config/users/" . $this->user . ".ini", $userFile, LOCK_EX);
Expand Down
7 changes: 7 additions & 0 deletions lang/en_US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,10 @@ add_user = "Add user"
username = "Username"
role = "Role"
change_password = "Change password"
config_mfa = "Configure MFA"
mfacode = "MFA Code"
verify_code = "Verify the MFA code"
verify_password = "Verify current password"
manualsetupkey = "You can also manually add the setup key"
mfa_error = "MFA code is not correct"
disablemfa = "Disable MFA"
14 changes: 9 additions & 5 deletions system/admin/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ function user($key, $user = null)
}

// Update the user
function update_user($userName, $password, $role)
function update_user($userName, $password, $role, $mfa_secret)
{
$file = 'config/users/' . $userName . '.ini';
if (file_exists($file)) {
file_put_contents($file, "password = " . password_hash($password, PASSWORD_DEFAULT) . "\n" .
"encryption = password_hash\n" .
"role = " . $role . "\n", LOCK_EX);
"role = " . $role . "\n" .
"mfa_secret = " . $mfa_secret . "\n", LOCK_EX);
return true;
}
return false;
Expand All @@ -38,7 +39,8 @@ function create_user($userName, $password, $role)
} else {
file_put_contents($file, "password = " . password_hash($password, PASSWORD_DEFAULT) . "\n" .
"encryption = password_hash\n" .
"role = " . $role . "\n", LOCK_EX);
"role = " . $role . "\n" .
"mfa_secret = none\n", LOCK_EX);
return true;
}
}
Expand All @@ -63,7 +65,8 @@ function session($user, $pass)
if (password_verify($pass, $user_pass)) {
if (session_status() == PHP_SESSION_NONE) session_start();
if (password_needs_rehash($user_pass, PASSWORD_DEFAULT)) {
update_user($user, $pass, $user_role);
$mfa = user('mfa_secret', $user);
update_user($user, $pass, $user_role, $mfa);
}
$_SESSION[site_url()]['user'] = $user;
header('location: admin');
Expand All @@ -72,7 +75,8 @@ function session($user, $pass)
}
} else if (old_password_verify($pass, $user_enc, $user_pass)) {
if (session_status() == PHP_SESSION_NONE) session_start();
update_user($user, $pass, $user_role);
$mfa = user('mfa_secret', $user);
update_user($user, $pass, $user_role, $mfa);
$_SESSION[site_url()]['user'] = $user;
header('location: admin');
} else {
Expand Down
59 changes: 59 additions & 0 deletions system/admin/views/edit-mfa.html.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php if (!defined('HTMLY')) die('HTMLy'); ?>
<?php
if (isset($_SESSION[site_url()]['user'])) {
$user = $_SESSION[site_url()]['user'];
}

use PragmaRX\Google2FA\Google2FA;
use BaconQrCode\Renderer\GDLibRenderer;
use BaconQrCode\Writer;

if (user('mfa_secret', $user) == 'disabled') {
$google2fa = new Google2FA();
$mfasecret = $google2fa->generateSecretKey();

$g2faUrl = $google2fa->getQRCodeUrl(
$user,
site_url(),
$mfasecret
);

$renderer = new GDLibRenderer(400);
$writer = new Writer($renderer);

$qrcode_image = base64_encode($writer->writeString($g2faUrl));
}
?>
<h2><?php echo i18n('config_mfa'); echo ': ' . $user; ?></h2>
<br>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?php echo get_csrf(); ?>">
<input type="hidden" name="username" value="<?php echo $user; ?>">
<?php if (user('mfa_secret', $user) == 'disabled') {
echo '<div style="text-align:center;width:100%;"><img style="margin:-10px auto;" src="data:image/png;base64, '.$qrcode_image.' "/></div>
<span style="text-align:center;width:100%;float:left;"><small>'.i18n('manualsetupkey').': '.$mfasecret.'</small></span>
<div class="form-group row">
<label for="site.url" class="col-sm-2 col-form-label">'.i18n('MFACode').'</label>
<div class="col-sm-10">
<input type="text" name="mfacode" class="form-control" id="mfacode" value="" placeholder="'.i18n('verify_code').'">
</div>
</div>
<div class="form-group row">
<label for="site.url" class="col-sm-2 col-form-label">'.i18n('Password').'</label>
<div class="col-sm-10">
<input type="password" name="password" class="form-control" id="password" value="" placeholder="'.i18n('verify_password').'">
</div>
</div>
<input type="hidden" name="mfa_secret" value="'.$mfasecret.'">
<input type="submit" class="btn btn-primary" style="width:100px;" value="'.i18n('Save').'">';
} else {
echo '<input type="hidden" name="mfa_secret" value="disabled">
<div class="form-group row">
<label for="site.url" class="col-sm-2 col-form-label">'.i18n('Password').'</label>
<div class="col-sm-10">
<input type="password" name="password" class="form-control" id="password" value="" placeholder="'.i18n('verify_password').'">
</div>
</div>
<input type="submit" class="btn btn-primary" style="width:100px;" value="'.i18n('disablemfa').'">';
} ?>
</form>
7 changes: 7 additions & 0 deletions system/admin/views/layout.html.php
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@
</p>
</a>
</li>
<li class="nav-item">
<a href="<?php echo site_url();?>edit/mfa" class="nav-link">
<p>
<?php echo i18n('config_mfa');?>
</p>
</a>
</li>
<li class="nav-item">
<a href="<?php echo site_url();?>edit/profile" class="nav-link">
<p>
Expand Down
3 changes: 3 additions & 0 deletions system/admin/views/login.html.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
} ?>" name="password" placeholder="<?php echo i18n('Password'); ?>"/>
<br>
<input type="hidden" name="csrf_token" value="<?php echo get_csrf() ?>">
<label><?php echo i18n('MFACode');?></label>
<input type="text" class="form-control" name="mfacode" placeholder="<?php echo i18n('verify_code'); ?>"/>
<br>
<?php if (config('google.reCaptcha') === 'true'): ?>
<script src='https://www.google.com/recaptcha/api.js'></script>
<div class="g-recaptcha" data-sitekey="<?php echo config("google.reCaptcha.public"); ?>"></div>
Expand Down
151 changes: 130 additions & 21 deletions system/htmly.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php
if (!defined('HTMLY')) die('HTMLy');

use PragmaRX\Google2FA\Google2FA;

// Load the configuration file
config('source', $config_file);

Expand Down Expand Up @@ -124,26 +126,71 @@
$user = from($_REQUEST, 'user');
$pass = from($_REQUEST, 'password');
if ($proper && $captcha && !empty($user) && !empty($pass)) {

session($user, $pass);
$log = session($user, $pass);

if (!empty($log)) {

config('views.root', 'system/admin/views');

render('login', array(
'title' => generate_title('is_default', i18n('Login')),
'description' => i18n('Login') . ' ' . blog_title(),
'canonical' => site_url(),
'metatags' => generate_meta(null, null),
'error' => '<ul>' . $log . '</ul>',
'type' => 'is_login',
'is_login' => true,
'bodyclass' => 'in-login',
'breadcrumb' => '<a href="' . site_url() . '">' . config('breadcrumb.home') . '</a> &#187; ' . i18n('Login')
));
}

if (user('mfa_secret', $user) !== "disabled") {
$mfa_secret = user('mfa_secret', $user);
$mfacode = from($_REQUEST, 'mfacode');
$google2fa = new Google2FA();
if ($google2fa->verifyKey($mfa_secret, $mfacode, '1')) {
session($user, $pass);
$log = session($user, $pass);

if (!empty($log)) {

config('views.root', 'system/admin/views');

render('login', array(
'title' => generate_title('is_default', i18n('Login')),
'description' => i18n('Login') . ' ' . blog_title(),
'canonical' => site_url(),
'metatags' => generate_meta(null, null),
'error' => '<ul>' . $log . '</ul>',
'type' => 'is_login',
'is_login' => true,
'bodyclass' => 'in-login',
'breadcrumb' => '<a href="' . site_url() . '">' . config('breadcrumb.home') . '</a> &#187; ' . i18n('Login')
));
}
} else {
$message['error'] .= '<li class="alert alert-danger">' . i18n('MFA_Error') . '</li>';

config('views.root', 'system/admin/views');

render('login', array(
'title' => generate_title('is_default', i18n('Login')),
'description' => i18n('Login') . ' ' . blog_title(),
'canonical' => site_url(),
'metatags' => generate_meta(null, null),
'error' => '<ul>' . $message['error'] . '</ul>',
'username' => $user,
'password' => $pass,
'type' => 'is_login',
'is_login' => true,
'bodyclass' => 'in-login',
'breadcrumb' => '<a href="' . site_url() . '">' . config('breadcrumb.home') . '</a> &#187; ' . i18n('Login')
));
}
} else {
session($user, $pass);
$log = session($user, $pass);

if (!empty($log)) {

config('views.root', 'system/admin/views');

render('login', array(
'title' => generate_title('is_default', i18n('Login')),
'description' => i18n('Login') . ' ' . blog_title(),
'canonical' => site_url(),
'metatags' => generate_meta(null, null),
'error' => '<ul>' . $log . '</ul>',
'type' => 'is_login',
'is_login' => true,
'bodyclass' => 'in-login',
'breadcrumb' => '<a href="' . site_url() . '">' . config('breadcrumb.home') . '</a> &#187; ' . i18n('Login')
));
}
}
} else {
$message['error'] = '';
if (empty($user)) {
Expand Down Expand Up @@ -376,12 +423,13 @@
$new_password = from($_REQUEST, 'password');
$user = $_SESSION[site_url()]['user'];
$role = user('role', $user);
$mfa = user('mfa_secret', $user);
$old_password = user('password', $username);
if ($user === $username) {
$file = 'config/users/' . $user . '.ini';
if (file_exists($file)) {
if (!empty($new_password)) {
update_user($user, $new_password, $role);
update_user($user, $new_password, $role, $mfa);
}
}
$redir = site_url() . 'admin';
Expand All @@ -396,6 +444,67 @@
}
});

get('/edit/mfa', function () {
if (login()) {
config('views.root', 'system/admin/views');
render('edit-mfa', array(
'title' => generate_title('is_default', i18n('config_mfa')),
'description' => safe_html(strip_tags(blog_description())),
'canonical' => site_url(),
'metatags' => generate_meta(null, null),
'type' => 'is_profile',
'is_admin' => true,
'bodyclass' => 'edit-mfa',
'breadcrumb' => '<a href="' . site_url() . '">' . config('breadcrumb.home') . '</a> &#187; '. i18n('config_mfa'),
));
} else {
$login = site_url() . 'login';
header("location: $login");
}
});

post('/edit/mfa', function() {
$proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
if (login() && $proper) {
$username = from($_REQUEST, 'username');
$mfa_secret = from($_REQUEST, 'mfa_secret');
$user = $_SESSION[site_url()]['user'];
$role = user('role', $user);
$password = from($_REQUEST, 'password');
if ($user === $username) {
if ($mfa_secret !== "disabled") {
$mfacode = from($_REQUEST, 'mfacode');
$google2fa = new Google2FA();
if ($google2fa->verifyKey($mfa_secret, $mfacode, '1')) {
$file = 'config/users/' . $user . '.ini';
if (file_exists($file)) {
if (!empty($mfa_secret)) {
update_user($user, $password, $role, $mfa_secret);
}
}
$redir = site_url() . 'admin';
header("location: $redir");
} else {
$redir = site_url() . 'admin';
header("location: $redir");
}
} else {
$file = 'config/users/' . $user . '.ini';
if (file_exists($file)) {
update_user($user, $password, $role, 'disabled');
}
$redir = site_url() . 'admin';
header("location: $redir");
}
} else {
$redir = site_url();
header("location: $redir");
}
} else {
$login = site_url() . 'login';
header("location: $login");
}
});
// Edit the frontpage
get('/edit/frontpage', function () {

Expand Down