2fa: first draft

This commit is contained in:
slawkens
2025-06-22 08:34:30 +02:00
parent 0f48f12e2e
commit a66cafceab
19 changed files with 549 additions and 1 deletions

View File

@@ -0,0 +1,8 @@
CREATE TABLE `myaac_account_email_codes`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`account_id` int NOT NULL,
`code` varchar(6) NOT NULL,
`created_at` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;

27
system/migrations/47.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
// add the myaac_account_email_codes
/**
* @var OTS_DB_MySQL $db
*/
$up = function () use ($db) {
if (!$db->hasColumn('accounts', '2fa_type')) {
$db->addColumn('accounts', '2fa_type', "tinyint NOT NULL DEFAULT 0 AFTER `web_flags`");
}
// add myaac_account_email_codes table
if (!$db->hasTable(TABLE_PREFIX . 'account_email_codes')) {
$db->exec(file_get_contents(__DIR__ . '/46-account_email_codes.sql'));
}
};
$down = function () use ($db) {
if ($db->hasColumn('accounts', '2fa_type')) {
$db->dropColumn('accounts', '2fa_type');
}
//if ($db->hasTable(TABLE_PREFIX . 'account_email_codes')) {
// $db->dropTable(TABLE_PREFIX . 'account_email_codes');
//}
};

View File

@@ -0,0 +1,80 @@
<?php
/**
* 2-factor authentication
*
* @package MyAAC
* @author Slawkens <slawkens@gmail.com>
* @copyright 2019 MyAAC
* @link https://my-aac.org
*/
use MyAAC\TwoFactorAuth\TwoFactorAuth;
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Two Factor Authentication';
require __DIR__ . '/base.php';
csrfProtect();
/**
* @var OTS_Account $account_logged
*/
$step = isset($_REQUEST['step']) ?? '';
if ((!setting('core.mail_enabled')) && ACTION == 'email-code') {
$twig->display('error_box.html.twig', ['errors' => ['Account two-factor e-mail authentication disabled.']]);
return;
}
$twoFactorAuth = new TwoFactorAuth($account_logged);
if (ACTION == 'email-code') {
if ($step === 'verify') {
$code = $_POST['email-code'] ?? '';
if ($twoFactorAuth->getAuthGateway()->verifyCode($code)) {
$twoFactorAuth->getAuthGateway()->deleteOldCodes();
//session(['2fa_skip' => true]);
header('Location: account/manage');
exit;
}
}
else if ($step == 'resend') {
$twoFactorAuth->getAuthGateway()->resendEmailCode();
$twig->display('account.2fa.email_code.html.twig');
}
else if ($step == 'confirm-activate') {
$account2faCode = $account_logged->getCustomField('2fa_email_code');
$account2faCodeTimeout = $account_logged->getCustomField('2fa_email_code_timeout');
if (!empty($account2faCodeTimeout) && time() - (int)$account2faCodeTimeout < (24 * 60 * 60)) {
$postCode = $_POST['email-code'] ?? '';
if (!empty($account2faCode)) {
if (!empty($postCode)) {
if ($postCode == $account2faCode) {
$twig->display('account.2fa.email-code.success.html.twig');
}
}
else {
}
}
else {
$errors[] = 'Your account dont have 2fa E-Mail code sent.';
}
}
else {
$errors[] = 'E-Mail Code expired.';
}
}
}
if (!empty($errors)) {
$twig->display('error_box.html.twig', ['errors' => $errors]);
$twig->display('account.back_button.html.twig', [
'new_line' => true
]);
}

View File

@@ -17,6 +17,10 @@ if(!$logged)
if(!empty($errors))
$twig->display('error_box.html.twig', array('errors' => $errors));
if (defined('HIDE_LOGIN_BOX') && HIDE_LOGIN_BOX) {
return;
}
$twig->display('account.login.html.twig', array(
'redirect' => $_REQUEST['redirect'] ?? null,
'account' => USE_ACCOUNT_NAME ? 'Name' : 'Number',

View File

@@ -10,6 +10,7 @@
*/
use MyAAC\RateLimit;
use MyAAC\TwoFactorAuth\TwoFactorAuth;
defined('MYAAC') or die('Direct access not allowed!');
@@ -57,6 +58,11 @@ if(!empty($login_account) && !empty($login_password))
setSession('remember_me', true);
}
$twoFactorAuth = new TwoFactorAuth($account_logged);
if (!$twoFactorAuth->process()) {
return;
}
$logged = true;
$logged_flags = $account_logged->getWebFlags();

View File

@@ -0,0 +1,14 @@
<?php
namespace MyAAC\Models;
use Illuminate\Database\Eloquent\Model;
class AccountEMailCode extends Model {
protected $table = TABLE_PREFIX . 'account_email_codes';
public $timestamps = false;
protected $fillable = ['account_id', 'code', 'created_at'];
}

View File

@@ -0,0 +1,13 @@
<?php
namespace MyAAC\TwoFactorAuth\Gateway;
use MyAAC\TwoFactorAuth\Interface\AuthGatewayInterface;
class AppAuthGateway extends BaseAuthGateway implements AuthGatewayInterface
{
public function verifyCode(string $code): bool
{
return true;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace MyAAC\TwoFactorAuth\Gateway;
class BaseAuthGateway
{
protected \OTS_Account $account;
public function __construct(\OTS_Account $account) {
$this->account = $account;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace MyAAC\TwoFactorAuth\Gateway;
use MyAAC\Models\AccountEMailCode;
use MyAAC\TwoFactorAuth\Interface\AuthGatewayInterface;
use MyAAC\TwoFactorAuth\TwoFactorAuth;
class EmailAuthGateway extends BaseAuthGateway implements AuthGatewayInterface
{
public function verifyCode(string $code): bool
{
return AccountEMailCode::where('account_id', '=', $this->account->getId())->where('code', $code)->where('created_at', '>', time() - TwoFactorAuth::EMAIL_CODE_VALID_UNTIL)->first() !== null;
}
public function hasRecentEmailCode(): bool {
return AccountEMailCode::where('account_id', '=', $this->account->getId())->where('created_at', '>', time() - TwoFactorAuth::EMAIL_CODE_VALID_UNTIL)->first() !== null;
}
public function deleteOldCodes(): void {
AccountEMailCode::where('account_id', '=', $this->account->getId())->delete();
}
public function resendEmailCode(): void
{
global $twig;
$newCode = generateRandomString(6, true, false, true);
AccountEMailCode::create([
'account_id' => $this->account->getId(),
'code' => $newCode,
'created_at' => time(),
]);
$mailBody = $twig->render('mail.account.2fa.email-code.html.twig', [
'code' => $newCode,
]);
if (!_mail($this->account->getEMail(), configLua('serverName') . ' - Requested Authentication Email Code', $mailBody)) {
error('An error occurred while sending email. For Admin: More info can be found in system/logs/mailer-error.log');
}
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace MyAAC\TwoFactorAuth\Interface;
interface AuthGatewayInterface
{
public function __construct(\OTS_Account $account);
public function verifyCode(string $code): bool;
}

View File

@@ -0,0 +1,63 @@
<?php
namespace MyAAC\TwoFactorAuth;
use MyAAC\Models\AccountEMailCode;
use MyAAC\TwoFactorAuth\Gateway\AppAuthGateway;
use MyAAC\TwoFactorAuth\Gateway\EmailAuthGateway;
use MyAAC\TwoFactorAuth\Interface\AuthGatewayInterface;
class TwoFactorAuth
{
const TYPE_NONE = 0;
const TYPE_EMAIL = 1;
const TYPE_APP = 2;
const EMAIL_CODE_VALID_UNTIL = 24 * 60 * 60;
private \OTS_Account $account;
private int $authType;
private EmailAuthGateway|AppAuthGateway $authGateway;
public function __construct(\OTS_Account $account) {
$this->account = $account;
$this->authType = (int)$this->account->getCustomField('2fa_type');
if ($this->authType === self::TYPE_EMAIL) {
$this->authGateway = new EmailAuthGateway($account);
}
else if ($this->authType === self::TYPE_APP) {
$this->authGateway = new AppAuthGateway($account);
}
}
public function process()
{
global $twig;
if ($this->authType == TwoFactorAuth::TYPE_EMAIL) {
if (!$this->authGateway->hasRecentEmailCode()) {
$this->authGateway->resendEmailCode();
success('Resent email.');
}
define('HIDE_LOGIN_BOX', true);
$twig->display('account.2fa.email-code.login.html.twig');
return false;
}
return true;
}
public function isActive(): bool {
return $this->authType != self::TYPE_NONE;
}
public function getAuthType(): int {
return $this->authType;
}
public function getAuthGateway(): AppAuthGateway|EmailAuthGateway {
return $this->authGateway;
}
}

View File

@@ -0,0 +1,59 @@
{% set title = 'Enter Email Code' %}
{% set content %}
<table style="width:100%;">
<tbody>
<tr>
<td>
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>
<div style="float: right;">
<form
action="{{ getLink('account/2fa') }}?action=email-code&step=resend"
method="post"
style="padding:0;margin:0;"
>
{{ csrf() }}
{% set button_name = 'Resend Email Code' %}
{{ include('buttons.base.html.twig') }}
</form>
</div>
An <b>email code</b> has already been sent to the email address assigned to your account.
Please check your email account's spam/junk filter and make sure that your mailbox is not
full.<br>In case you need a new email code, you can request one by clicking on "Resend Email
Code".
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
<tr>
<td>
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td><b>Email code authentication is activated for your account.</b><br><br>Please enter the <b>most
recent email code</b> you have received in order to log in.<br>
<div style="margin-top: 15px; margin-bottom: 15px;">
<div class="LabelV150" style="float:left;">Email Code:</div>
<form method="post" action="{{ getLink('account/2fa') }}?action=email-code&step=verify">
{{ csrf() }}
<input name="email-code" maxlength="15" autocomplete="off">
</form>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
{% endset %}
{% include 'tables.headline.html.twig' %}

View File

@@ -0,0 +1,15 @@
{% set title = 'Email Code Authentication Activated' %}
{% set content %}
<table style="width:100%;">
<tbody>
<tr>
<td>You have successfully activated <b>email code authentication</b> for your account. This means an <b>email
code</b> will be sent to the email address assigned to your account whenever you try to log in to the
{{ config.lua.serverName }} client or the {{ config.lua.serverName }} website. In order to log in, you will need to enter the <b>most recent email code</b> you have received.
</td>
</tr>
</tbody>
</table>
{% endset %}
{% include 'tables.headline.html.twig' %}
{{ include('account.back_button.html.twig') }}

View File

@@ -0,0 +1,9 @@
Dear {{ config.lua.serverName}} player,
<br/><br/>
Your account is protected by email code authentication, and you requested a new email code:
<br/><br/>
<p>{{ code }}</p>
<br/>
Note that the code is only valid for 24 hours.
<br/><br/>
Kind Regards,

View File

@@ -0,0 +1,5 @@
Dear {{ config.lua.serverName}} player,<br/>
<br/>
A <strong>wrong two-factor authentication code</strong> was entered for your {{ config.lua.serverName}} account. If you simply mistyped the code, please try again.<br/>
<br/>
However, if this was <strong>not you</strong>, someone else may be trying to access your account. Since they already know your password, we strongly recommend that you <strong>change your password immediately</strong>.