[WIP] Working app auth (Still not ready)

Missing rec key validation
Doesn't work with google recaptcha plugin
This commit is contained in:
slawkens
2026-01-18 21:45:50 +01:00
parent 2e4a8c3d3d
commit 21e2eed640
22 changed files with 428 additions and 35 deletions

View File

@@ -103,6 +103,11 @@ if(!$db->hasColumn('accounts', '2fa_type')) {
success($locale['step_database_adding_field'] . ' accounts.2fa_type...');
}
if(!$db->hasColumn('accounts', '2fa_secret')) {
if(query("ALTER TABLE `accounts` ADD `2fa_secret` varchar(16) NOT NULL DEFAULT '' AFTER `2fa_type`;"))
success($locale['step_database_adding_field'] . ' accounts.2fa_secret...');
}
if(!$db->hasColumn('accounts', 'email_verified')) {
if(query("ALTER TABLE `accounts` ADD `email_verified` TINYINT(1) NOT NULL DEFAULT 0 AFTER `web_flags`;"))
success($locale['step_database_adding_field'] . ' accounts.email_verified...');

View File

@@ -11,6 +11,10 @@ $up = function () use ($db) {
$db->addColumn('accounts', '2fa_type', "tinyint NOT NULL DEFAULT 0 AFTER `web_flags`");
}
if (!$db->hasColumn('accounts', '2fa_secret')) {
$db->addColumn('accounts', '2fa_secret', "varchar(16) NOT NULL DEFAULT '' AFTER `2fa_type`");
}
// 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'));
@@ -22,6 +26,10 @@ $down = function () use ($db) {
$db->dropColumn('accounts', '2fa_type');
}
if ($db->hasColumn('accounts', '2fa_secret')) {
$db->dropColumn('accounts', '2fa_secret');
}
if ($db->hasTable(TABLE_PREFIX . 'account_email_codes')) {
$db->dropTable(TABLE_PREFIX . 'account_email_codes');
}

View File

@@ -1,4 +0,0 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
require __DIR__ . '/../base.php';

View File

@@ -1,5 +0,0 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
require __DIR__ . '/../base.php';

View File

@@ -0,0 +1,24 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
require __DIR__ . '/../base.php';
$account = \MyAAC\Models\Account::find($account_logged->getId());
if (!$account) {
error('Account not found!');
return;
}
if ($db->hasColumn('accounts', 'secret')) {
$account->secret = NULL;
}
$account->{'2fa_secret'} = '';
$account->save();
$twoFactorAuth->disable();
$twig->display('success.html.twig', [
'title' => 'Disabled',
'description' => 'Two Factor Authentication has been disabled.'
]);

View File

@@ -0,0 +1,94 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
use MyAAC\TwoFactorAuth\TwoFactorAuth;
use OTPHP\TOTP;
use Symfony\Component\Clock\NativeClock;
require __DIR__ . '/../base.php';
if (!empty($account_logged->getCustomField('2fa_secret'))) {
$twig->display('account/2fa/app/enable.already_connected.html.twig');
return;
}
if (ACTION == 'request') {
$clock = new NativeClock();
$secret = generateRandom2faSecret();
$otp = TOTP::createFromSecret($secret);
setSession('2fa_secret', $secret);
$otp->setLabel($account_logged->getEmail());
$otp->setIssuer(configLua('serverName'));
$grCodeUri = $otp->getQrCodeUri(
'https://api.qrserver.com/v1/create-qr-code/?data=[DATA]&size=200x200&ecc=M',
'[DATA]'
);
$twig->display('account/2fa/app/enable.html.twig', [
'grCodeUri' => $grCodeUri,
'secret' => $secret,
]);
return;
}
if (ACTION == 'link') {
$secret = getSession('2fa_secret');
if ($secret === null) {
$twig->display('error_box.html.twig', ['errors' => ['Secret not set']]);
}
$totp = $_POST['totp'] ?? '';
if (!empty($totp)) {
$otp = TOTP::createFromSecret($secret);
$otp->setLabel($account_logged->getEmail());
$otp->setIssuer(configLua('serverName'));
if (!$otp->verify($totp)) {
$grCodeUri = $otp->getQrCodeUri(
'https://api.qrserver.com/v1/create-qr-code/?data=[DATA]&size=200x200&ecc=M',
'[DATA]'
);
$twig->display('error_box.html.twig', ['errors' => ['Token is invalid!']]);
$twig->display('account/2fa/app/enable.html.twig', [
'grCodeUri' => $grCodeUri,
'secret' => $secret,
]);
return;
}
if ($db->hasColumn('accounts', 'secret')) {
$account_logged->setCustomField('secret', $secret);
}
$account_logged->setCustomField('2fa_secret', $secret);
$twoFactorAuth->enable(TwoFactorAuth::TYPE_APP);
$twig->display('success.html.twig',
[
'title' => 'Authenticator App Connected',
'description' => 'You successfully connected your Tibia account to an authenticator app.'
]
);
return;
}
}
if (!empty($errors)) {
$twig->display('error_box.html.twig', ['errors' => $errors]);
}
$twig->display('account/2fa/app/enable.warning.html.twig', ['wrongCode' => count($errors) > 0]);

View File

@@ -27,3 +27,20 @@ if (!isset($account_logged) || !$account_logged->isLoaded()) {
$twoFactorAuth = TwoFactorAuth::getInstance($account_logged);
$twig->addGlobal('account_logged', $account_logged);
/**
* Took from ZnoteAAC
* @author Znote
*/
function generateRandom2faSecret($length = 16): string
{
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}

View File

@@ -36,4 +36,4 @@ if (!empty($errors)) {
$twig->display('error_box.html.twig', ['errors' => $errors]);
}
$twig->display('account/2fa/email/request.html.twig', ['wrongCode' => count($errors) > 0]);
$twig->display('account/2fa/email/enable.html.twig', ['wrongCode' => count($errors) > 0]);

View File

@@ -3,11 +3,17 @@
namespace MyAAC\TwoFactorAuth\Gateway;
use MyAAC\TwoFactorAuth\Interface\AuthGatewayInterface;
use OTPHP\TOTP;
class AppAuthGateway extends BaseAuthGateway implements AuthGatewayInterface
{
public function verifyCode(string $code): bool
{
return true;
$otp = TOTP::createFromSecret($this->account->getCustomField('secret'));
$otp->setLabel($this->account->getEmail());
$otp->setIssuer(configLua('serverName'));
return $otp->verify($code);
}
}

View File

@@ -52,23 +52,26 @@ class TwoFactorAuth
return true;
}
$view = 'app';
if ($this->authType == self::TYPE_EMAIL) {
$view = 'email';#
}
if (empty($code)) {
if ($this->authType == self::TYPE_EMAIL) {
if (!$this->hasRecentEmailCode(15 * 60)) {
$this->resendEmailCode();
//success('Resent email.');
}
}
define('HIDE_LOGIN_BOX', true);
$twig->display('account/2fa/email/login.html.twig', [
'account_login' => $login_account,
'password_login' => $login_password,
'remember_me' => $remember_me,
]);
}
else {
echo 'Two Factor App Auth';
}
define('HIDE_LOGIN_BOX', true);
$twig->display("account/2fa/$view/login.html.twig", [
'account_login' => $login_account,
'password_login' => $login_password,
'remember_me' => $remember_me,
]);
return false;
}
@@ -91,10 +94,16 @@ class TwoFactorAuth
define('HIDE_LOGIN_BOX', true);
$errors[] = 'Invalid email code!';
if ($this->authType == self::TYPE_APP) {
$errors[] = 'The token is invalid!';
}
else {
$errors[] = 'Invalid email code!';
}
$twig->display('error_box.html.twig', ['errors' => $errors]);
$twig->display('account/2fa/email/login.html.twig',
$twig->display("account/2fa/$view/login.html.twig",
[
'account_login' => $login_account,
'password_login' => $login_password,
@@ -120,14 +129,14 @@ class TwoFactorAuth
{
$twoFactorView = 'account/2fa/protected.html.twig';
if ($this->authType == self::TYPE_EMAIL) {
$twoFactorView2 = 'account/2fa/email/activated.html.twig';
$twoFactorView2 = 'account/2fa/email/enabled.html.twig';
}
elseif ($this->authType == self::TYPE_APP) {
$twoFactorView2 = 'account/2fa/app/activated.html.twig';
$twoFactorView2 = 'account/2fa/app/enabled.html.twig';
}
else {
$twoFactorView = 'account/2fa/connect.html.twig';
$twoFactorView2 = 'account/2fa/email/activate.html.twig';
$twoFactorView2 = 'account/2fa/email/manage.html.twig';
}
return [$twoFactorView, $twoFactorView2];

View File

@@ -0,0 +1,22 @@
{% set title = 'Disable Two Factor App' %}
{% set background = config('darkborder') %}
{% set content %}
<table style="width:100%;">
<tbody>
<tr>
<td>
Two-factor authentication is already enabled on your account.<br/>
Click the button to disable the two-factor app.<br/><br/>
<form action="{{ getLink('account/2fa/app/disable') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_name = 'Disable' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
</tr>
</tbody>
</table>
{% endset %}
{% include 'tables.headline.html.twig' %}

View File

@@ -0,0 +1,69 @@
<table style="width:100%;">
<tbody>
<tr>
<td>
<div class="TableContentContainer ">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>
<ol>
<li>Open an authenticator app of your choice (e.g. <a
target="_blank"
href="https://support.google.com/accounts/answer/1066447"
rel="noopener noreferrer">Google Authenticator</a>, <a
target="_blank" href="https://www.authy.com/users"
rel="noopener noreferrer">Authy</a>). In the app you
will be asked either to enter a key manually:<br><b>{{ secret }}</b><br>or
to scan the barcode below:<br>
<img alt="QR code" style="margin-top: 15px; margin-bottom: 15px;"
src="{{ grCodeUri }}">
</li>
<li><label for="totp">Enter the verification code you have received from the used
authenticator app:</label><br>
<div style="margin-top: 15px; margin-bottom: 15px;">
<input form="form" id="totp" name="totp" autocomplete="off"></div>
</li>
<li>Click on "Continue" to connect the authenticator app to your
Tibia account.
</li>
</ol>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
<br>
<table style="width: 100%;">
<tbody>
<tr align="center" valign="top">
<td>
<form id="form" method="post" action="{{ getLink('account/2fa/app/enable') }}">
<input type="hidden" name="action" value="link">
{{ csrf() }}
{% set button_color = 'green' %}
{% set button_name = 'Continue' %}
{{ include('buttons.base.html.twig') }}
</td>
<td>
<form action="{{ getLink('account/manage') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_color = 'blue' %}
{% set button_name = 'Request' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,81 @@
{% set title = 'Warning' %}
{% set background = config('darkborder') %}
{% set content %}
<table style="width:100%;">
<tbody>
<tr>
<td>
<span class="red"><b>Please read this warning carefully as it contains important security information! If you skip this message, you might lose your Tibia account!</b></span><br><br>
<p>Before you connect your account with an authenticator app, you will be asked to
enter your recovery key. If you do not have a valid recovery key, you need to
order a new one before you can connect your account with an authenticator.</p>
<p>Why?<br>The recovery key is the only way to unlink the authenticator app from
your Tibia account in various cases, among others, if:</p>
<ul style="list-style-type:square">
<li>you lose your device (mobile phone, tablet, etc.) with the authenticator
app
</li>
<li>the device with the authenticator app does not work anymore</li>
<li>the device with the authenticator app gets stolen</li>
<li>you delete the authenticator app from your device and reinstall it</li>
<li>your device is reset for some reason</li>
</ul>
<p></p>
<p>Please note that the authenticator app data is not saved on your device's account
(e.g. Google or iTunes sync) even if you have app data backup&amp;synchronisation
activated in the settings of your device!</p>
<p>In all these scenarios, the recovery key is the only way to get access to your
Tibia account. Note that not even customer support will be able to help you in
these cases if you do not have a valid recovery key.<br>For this reason, make
sure to store your recovery key always in a safe place!</p><br>Do you have a
valid recovery key and would like to request the email with the confirmation key to
start connecting your Tibia account to an authenticator app?<br><br><b>Enter your
recovery key:</b><br>
<div style="margin-top: 15px; margin-bottom: 15px;">
<input class="UpperCaseInput" name="key1" value="" size="5" maxlength="5" autocomplete="off"> -
<input class="UpperCaseInput" name="key2" value="" size="5" maxlength="5" autocomplete="off"> - <input class="UpperCaseInput" name="key3" value="" size="5" maxlength="5" autocomplete="off"> -
<input class="UpperCaseInput" name="key4" value="" size="5" maxlength="5" autocomplete="off"></div>
</td>
</tr>
</tbody>
</table>
{% endset %}
{% include 'tables.headline.html.twig' %}
<br>
<table style="width:100%;">
<tbody>
<tr align="center">
<td>
<form action="{{ getLink('account/2fa/app/enable') }}" method="post" style="padding:0;margin:0;">
<input type="hidden" name="action" value="request" />
{{ csrf() }}
{% set button_name = 'Request' %}
{{ include('buttons.base.html.twig') }}
</td>
<td>
<form action="{{ getLink('account/register') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_name = 'Order Recovery Key' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
<td>
<form action="{{ getLink('account/manage') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_name = 'Cancel Request' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1 @@
TODO

View File

@@ -0,0 +1,66 @@
{% set title = 'Enter Authenticator App Token' %}
{% set background = config('darkborder') %}
{% 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 verification code generated by the app:<br>
<div style="margin-top: 15px; margin-bottom: 15px;">
<div class="LabelV200" style="float:left;">Authenticator App
Token:
</div>
<input form="form-code" id="auth-code" name="auth-code" maxlength="6" autocomplete="off"></div>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
{% endset %}
{% include 'tables.headline.html.twig' %}
<br/>
<table style="width: 100%;">
<tbody>
<tr align="center" valign="top">
<td>
<form id="form-code" action="{{ getLink('account/manage') }}" method="post">
{{ csrf() }}
<input type="hidden" name="account_login" value="{{ account_login ?? '' }}" />
<input type="hidden" name="password_login" value="{{ password_login ?? '' }}" />
{% if remember_me %}
<input type="hidden" name="remember_me" value="true" />
{% endif %}
<input type="hidden" name="step" value="verify">
{% set button_color = 'green' %}
{% set button_name = 'Continue' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
<td>
<form action="{{ getLink('account/manage') }}" method="post"
style="padding:0;margin:0;">
{{ csrf() }}
{% set button_color = 'blue' %}
{% set button_name = 'Cancel' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
</tr>
</tbody>
</table>

View File

@@ -9,7 +9,7 @@
<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/app/activate') }}" method="post" style="margin: 0; padding: 0;">
<form action="{{ getLink('account/2fa/app/enable') }}" method="post" style="margin: 0; padding: 0;">
{{ csrf() }}
{% set button_name = 'Request' %}
{% include('buttons.base.html.twig') %}

View File

@@ -8,7 +8,7 @@
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>To deactivate <b>two-factor email code authentication</b> for your account, enter the
<td>To disable <b>two-factor email code authentication</b> for your account, enter the
received <b>email code</b> below. Note, however, that <b>email code authentication</b>
is an important security feature which helps to prevent any unauthorized access to your
Tibia account.
@@ -86,7 +86,7 @@
<tbody>
<tr align="center" valign="top">
<td>
<form id="form-code" method="post" action="{{ getLink('account/2fa/email/deactivate') }}">
<form id="form-code" method="post" action="{{ getLink('account/2fa/email/disable') }}">
{{ csrf() }}
<input type="hidden" name="save" value="1">

View File

@@ -9,7 +9,7 @@
<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
<td>Enter the email code below to enable <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
@@ -87,7 +87,7 @@
<tbody>
<tr align="center" valign="top">
<td>
<form id="confirmActivateForm" action="{{ getLink('account/2fa/email/activate') }}" method="post" style="padding:0;margin:0;">
<form id="confirmActivateForm" action="{{ getLink('account/2fa/email/enable') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
<input type="hidden" name="save" value="1">

View File

@@ -6,7 +6,7 @@
<tr>
<td>
<div style="float: right; width: 135px;">
<form action="{{ getLink('account/2fa/email/deactivate') }}" method="post" style="padding:0;margin:0;">
<form action="{{ getLink('account/2fa/email/disable') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_name = 'Deactivate' %}
@@ -14,7 +14,7 @@
</form>
</div>
<b>Two-Factor Email Code Authentication <span style="color: green">Activated</span>!</b>
<p>To deactivate <b>email code authentication</b>, click on the "Deactivate" button.</p>
<p>To disable <b>email code authentication</b>, click on the "Deactivate" button.</p>
<!--p>You will have to confirm the deactivation by entering an <b>email code</b> which will be sent
to the email address assigned to your account.</p-->
</td>

View File

@@ -10,7 +10,7 @@
<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/email/activate') }}" method="post" style="margin: 0; padding: 0;">
<form action="{{ getLink('account/2fa/email/enable') }}" method="post" style="margin: 0; padding: 0;">
{{ csrf() }}
{% set button_name = 'Request' %}
{% include('buttons.base.html.twig') %}
@@ -20,7 +20,7 @@
</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>
<p>As a first step to enable <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>