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

@ -27,7 +27,7 @@ if (version_compare(phpversion(), '8.1', '<')) die('PHP version 8.1 or higher is
const MYAAC = true;
const MYAAC_VERSION = '1.6.1';
const DATABASE_VERSION = 45;
const DATABASE_VERSION = 47;
const TABLE_PREFIX = 'myaac_';
define('START_TIME', microtime(true));
define('MYAAC_OS', stripos(PHP_OS, 'WIN') === 0 ? 'WINDOWS' : (strtoupper(PHP_OS) === 'DARWIN' ? 'MAC' : 'LINUX'));

View File

@ -10,6 +10,15 @@ CREATE TABLE `myaac_account_actions`
KEY (`account_id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;
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;
CREATE TABLE `myaac_admin_menu`
(
`id` int NOT NULL AUTO_INCREMENT,

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

View File

@ -0,0 +1,90 @@
{% set title = 'Activate Email Code Authentication' %}
{% 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>Enter the email code below to activate <b>two-factor email code authentication</b>. Note
that this code is only valid for 24 hours.<br><br>
<div class="AttentionSign"><img src="{{ template_path }}/images/global/content/attentionsign.gif"></div>
<b>Note:</b> Once you have email code authentication activated, an <b>email code</b> will be
sent to the email address assigned to your account whenever you try to log in to the Tibia
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>
</div>
</td>
</tr>
<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=emailcode&step=resend"
method="post" style="padding:0px;margin:0px;">
{{ 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>To complete the activation of email code authentication for your Tibia account, please enter
the email code you received at the email address assigned to your account.
<div style="margin-top: 15px; margin-bottom: 15px;">
<div class="LabelV150" style="float:left;">Email Code:</div>
<input form="confirmActivateForm" name="email-code" maxlength="15">
</div>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
{% endset %}
<table style="width: 100%;">
<tbody>
<tr align="center" valign="top">
<td>
<form id="confirmActivateForm" action="{{ getLink('account/2fa') }}?action=email-code" method="post" style="padding:0;margin:0;">
<input type="hidden" name="step" value="confirm-activate">
{% set button_color = 'green' %}
{{ include('buttons.submit.html.twig') }}
</form>
</td>
<td>
<form action="{{ getLink('account/manage') }}" method="post" style="padding:0;margin:0;">
{{ include('buttons.back.html.twig') }}
</form>
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,81 @@
{% set title = 'Two-Factor Authentication' %}
{% set content %}
<table style="width:100%;">
<tbody>
<tr>
<td>
<div class="TableShadowContainerRightTop">
<div class="TableShadowRightTop" style="background-image:url({{ template_path }}/images/global/content/table-shadow-rt.gif);"></div>
</div>
<div class="TableContentAndRightShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-rm.gif);">
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody><tr>
<td class="LabelV"><b>Connect your {{ config.lua.serverName }} account to an authenticator app!</b>
<div style="float: right; font-size: 1px;">
<form action="{{ getLink('account/2fa') }}?action=email-code" method="post" style="margin: 0px; padding: 0px;">
{{ csrf() }}
{% set button_name = 'Request' %}
{% include('buttons.base.html.twig') %}
</form>
</div>
</td>
</tr>
<tr>
<td>
<p>As a first step to connect an <b>authenticator app</b> to your account, click on "Request"! An email with a confirmation key will be sent to the email address assigned to your account.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="TableShadowContainer">
<div class="TableBottomShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-bm.gif);">
<div class="TableBottomLeftShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-bl.gif);"></div>
<div class="TableBottomRightShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-br.gif);"></div>
</div>
</div>
</td>
</tr>
<tr>
<td>
<div class="TableShadowContainerRightTop">
<div class="TableShadowRightTop" style="background-image:url({{ template_path }}/images/global/content/table-shadow-rt.gif);"></div>
</div>
<div class="TableContentAndRightShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-rm.gif);">
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody><tr>
<td class="LabelV"><b>Activate email code authentication for your account!</b>
<div style="float: right; font-size: 1px;">
<form action="{{ getLink('account/2fa') }}?action=email-code" method="post" style="margin: 0; padding: 0;">
{{ csrf() }}
{% set button_name = 'Request' %}
{% include('buttons.base.html.twig') %}
</form>
</div>
</td>
</tr>
<tr>
<td>
<p>As a first step to activate <b>email code authentication</b> for your account, click on "Request"! An <b>email code</b> will be sent to the email address assigned to your account. You will be asked to enter this <b>email code</b> on the next page within 24 hours.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="TableShadowContainer">
<div class="TableBottomShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-bm.gif);">
<div class="TableBottomLeftShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-bl.gif);"></div>
<div class="TableBottomRightShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-br.gif);"></div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
{% endset %}
{% include('tables.headline.html.twig') %}