Added MFA support

Added the option to enable/disable TOTP MFA per user using QR code or manually entering a key.
This commit is contained in:
KuJoe 2024-05-17 11:56:45 -04:00
commit 05ddfa9114
8 changed files with 220 additions and 26 deletions

View file

@ -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

View file

@ -145,11 +145,13 @@ class Settings
'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);

View file

@ -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"

View file

@ -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;
@ -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;
}
}
@ -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');
@ -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 {

View file

@ -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>

View file

@ -243,6 +243,13 @@ if (isset($_GET['search'])) {
</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>

View file

@ -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>

View file

@ -1,6 +1,8 @@
<?php
if (!defined('HTMLY')) die('HTMLy');
use PragmaRX\Google2FA\Google2FA;
// Load the configuration file
config('source', $config_file);
@ -124,26 +126,71 @@ post('/login', function () {
$user = from($_REQUEST, 'user');
$pass = from($_REQUEST, 'password');
if ($proper && $captcha && !empty($user) && !empty($pass)) {
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);
session($user, $pass);
$log = session($user, $pass);
if (!empty($log)) {
if (!empty($log)) {
config('views.root', 'system/admin/views');
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>';
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')
));
}
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)) {
@ -376,12 +423,13 @@ post('/edit/password', function() {
$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';
@ -396,6 +444,67 @@ post('/edit/password', function() {
}
});
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 () {