diff --git a/install/tools/5-database.php b/install/tools/5-database.php index 1e5f24ae..74ddeaa8 100644 --- a/install/tools/5-database.php +++ b/install/tools/5-database.php @@ -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...'); diff --git a/system/migrations/50.php b/system/migrations/50.php index 112dddda..d6a78207 100644 --- a/system/migrations/50.php +++ b/system/migrations/50.php @@ -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'); } diff --git a/system/pages/account/2fa/app/activate.php b/system/pages/account/2fa/app/activate.php deleted file mode 100644 index 46e0c749..00000000 --- a/system/pages/account/2fa/app/activate.php +++ /dev/null @@ -1,4 +0,0 @@ -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.' +]); diff --git a/system/pages/account/2fa/app/enable.php b/system/pages/account/2fa/app/enable.php new file mode 100644 index 00000000..9a574fe3 --- /dev/null +++ b/system/pages/account/2fa/app/enable.php @@ -0,0 +1,94 @@ +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]); diff --git a/system/pages/account/2fa/base.php b/system/pages/account/2fa/base.php index 818db0fc..2b8c9e5b 100644 --- a/system/pages/account/2fa/base.php +++ b/system/pages/account/2fa/base.php @@ -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; +} diff --git a/system/pages/account/2fa/email/deactivate.php b/system/pages/account/2fa/email/disable.php similarity index 100% rename from system/pages/account/2fa/email/deactivate.php rename to system/pages/account/2fa/email/disable.php diff --git a/system/pages/account/2fa/email/activate.php b/system/pages/account/2fa/email/enable.php similarity index 92% rename from system/pages/account/2fa/email/activate.php rename to system/pages/account/2fa/email/enable.php index 97cab77e..53f353ff 100644 --- a/system/pages/account/2fa/email/activate.php +++ b/system/pages/account/2fa/email/enable.php @@ -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]); diff --git a/system/src/TwoFactorAuth/Gateway/AppAuthGateway.php b/system/src/TwoFactorAuth/Gateway/AppAuthGateway.php index 6774a632..13e074bd 100644 --- a/system/src/TwoFactorAuth/Gateway/AppAuthGateway.php +++ b/system/src/TwoFactorAuth/Gateway/AppAuthGateway.php @@ -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); } } diff --git a/system/src/TwoFactorAuth/TwoFactorAuth.php b/system/src/TwoFactorAuth/TwoFactorAuth.php index 80344758..c17fabbf 100644 --- a/system/src/TwoFactorAuth/TwoFactorAuth.php +++ b/system/src/TwoFactorAuth/TwoFactorAuth.php @@ -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]; diff --git a/system/templates/account/2fa/app/activate.html.twig b/system/templates/account/2fa/app/activate.html.twig deleted file mode 100644 index e69de29b..00000000 diff --git a/system/templates/account/2fa/app/enable.already_connected.html.twig b/system/templates/account/2fa/app/enable.already_connected.html.twig new file mode 100644 index 00000000..c6c86682 --- /dev/null +++ b/system/templates/account/2fa/app/enable.already_connected.html.twig @@ -0,0 +1,22 @@ +{% set title = 'Disable Two Factor App' %} +{% set background = config('darkborder') %} + +{% set content %} +
|
+ Two-factor authentication is already enabled on your account. + Click the button to disable the two-factor app. + + |
+
|
+
+
+
|
+
| + | ++ + | +
|
+ Please read this warning carefully as it contains important security information! If you skip this message, you might lose your Tibia account! + 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. +Why?
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&synchronisation + activated in the settings of your device! +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. 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? Enter your + recovery key: +
+
+ -
+ - -
+
+ |
+
| + | ++ + | ++ + | +
|
+
+
+
|
+
| + + | ++ + | +