mirror of
https://github.com/openvk/openvk
synced 2024-12-23 00:51:03 +03:00
Users: Add two-factor authentication (#321)
This commit adds full TOTP 2FA. And even backup codes modeled on the original VK
This commit is contained in:
parent
34f4f8fc5d
commit
e433e83c5d
15 changed files with 425 additions and 2 deletions
|
@ -327,6 +327,16 @@ class User extends RowModel
|
||||||
return (int)floor((time() - $this->getBirthday()) / mktime(0, 0, 0, 1, 1, 1971));
|
return (int)floor((time() - $this->getBirthday()) / mktime(0, 0, 0, 1, 1, 1971));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get2faSecret(): ?string
|
||||||
|
{
|
||||||
|
return $this->getRecord()["2fa_secret"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function is2faEnabled(): bool
|
||||||
|
{
|
||||||
|
return !is_null($this->get2faSecret());
|
||||||
|
}
|
||||||
|
|
||||||
function updateNotificationOffset(): void
|
function updateNotificationOffset(): void
|
||||||
{
|
{
|
||||||
$this->stateChanges("notification_offset", time());
|
$this->stateChanges("notification_offset", time());
|
||||||
|
@ -553,6 +563,38 @@ class User extends RowModel
|
||||||
return sizeof($this->getRecord()->related("gift_user_relations.receiver"));
|
return sizeof($this->getRecord()->related("gift_user_relations.receiver"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get2faBackupCodes(): \Traversable
|
||||||
|
{
|
||||||
|
$sel = $this->getRecord()->related("2fa_backup_codes.owner");
|
||||||
|
foreach($sel as $target)
|
||||||
|
yield $target->code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get2faBackupCodeCount(): int
|
||||||
|
{
|
||||||
|
return sizeof($this->getRecord()->related("2fa_backup_codes.owner"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate2faBackupCodes(): void
|
||||||
|
{
|
||||||
|
$codes = [];
|
||||||
|
|
||||||
|
for($i = 0; $i < 10 - $this->get2faBackupCodeCount(); $i++) {
|
||||||
|
$codes[] = [
|
||||||
|
owner => $this->getId(),
|
||||||
|
code => random_int(10000000, 99999999)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(sizeof($codes) > 0)
|
||||||
|
DatabaseConnection::i()->getContext()->table("2fa_backup_codes")->insert($codes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function use2faBackupCode(int $code): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->getRecord()->related("2fa_backup_codes.owner")->where("code", $code)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
function getSubscriptionStatus(User $user): int
|
function getSubscriptionStatus(User $user): int
|
||||||
{
|
{
|
||||||
$subbed = !is_null($this->getRecord()->related("subscriptions.follower")->where([
|
$subbed = !is_null($this->getRecord()->related("subscriptions.follower")->where([
|
||||||
|
|
|
@ -10,6 +10,7 @@ use Chandler\Session\Session;
|
||||||
use Chandler\Security\User as ChandlerUser;
|
use Chandler\Security\User as ChandlerUser;
|
||||||
use Chandler\Security\Authenticator;
|
use Chandler\Security\Authenticator;
|
||||||
use Chandler\Database\DatabaseConnection;
|
use Chandler\Database\DatabaseConnection;
|
||||||
|
use lfkeitel\phptotp\{Base32, Totp};
|
||||||
|
|
||||||
final class AuthPresenter extends OpenVKPresenter
|
final class AuthPresenter extends OpenVKPresenter
|
||||||
{
|
{
|
||||||
|
@ -132,9 +133,27 @@ final class AuthPresenter extends OpenVKPresenter
|
||||||
if(!$user)
|
if(!$user)
|
||||||
$this->flashFail("err", "Не удалось войти", "Неверное имя пользователя или пароль. <a href='/restore.pl'>Забыли пароль?</a>");
|
$this->flashFail("err", "Не удалось войти", "Неверное имя пользователя или пароль. <a href='/restore.pl'>Забыли пароль?</a>");
|
||||||
|
|
||||||
if(!$this->authenticator->login($user->id, $this->postParam("password")))
|
if(!$this->authenticator->verifyCredentials($user->id, $this->postParam("password")))
|
||||||
$this->flashFail("err", "Не удалось войти", "Неверное имя пользователя или пароль. <a href='/restore.pl'>Забыли пароль?</a>");
|
$this->flashFail("err", "Не удалось войти", "Неверное имя пользователя или пароль. <a href='/restore.pl'>Забыли пароль?</a>");
|
||||||
|
|
||||||
|
$secret = $user->related("profiles.user")->fetch()["2fa_secret"];
|
||||||
|
$code = $this->postParam("code");
|
||||||
|
if(!is_null($secret)) {
|
||||||
|
$this->template->_template = "Auth/LoginSecondFactor.xml";
|
||||||
|
$this->template->login = $this->postParam("login");
|
||||||
|
$this->template->password = $this->postParam("password");
|
||||||
|
|
||||||
|
if(is_null($code))
|
||||||
|
return;
|
||||||
|
|
||||||
|
$ovkUser = new User($user->related("profiles.user")->fetch());
|
||||||
|
if(!($code === (new Totp)->GenerateToken(Base32::decode($secret)) || $ovkUser->use2faBackupCode((int) $code))) {
|
||||||
|
$this->flash("err", "Не удалось войти", tr("incorrect_2fa_code"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->authenticator->authenticate($user->id);
|
||||||
$this->redirect($redirUrl ?? "/id" . $user->related("profiles.user")->fetch()->id, static::REDIRECT_TEMPORARY);
|
$this->redirect($redirUrl ?? "/id" . $user->related("profiles.user")->fetch()->id, static::REDIRECT_TEMPORARY);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
@ -177,7 +196,19 @@ final class AuthPresenter extends OpenVKPresenter
|
||||||
$this->redirect("/");
|
$this->redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->template->is2faEnabled = $request->getUser()->is2faEnabled();
|
||||||
|
|
||||||
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||||
|
if($request->getUser()->is2faEnabled()) {
|
||||||
|
$user = $request->getUser();
|
||||||
|
$code = $this->postParam("code");
|
||||||
|
$secret = $user->get2faSecret();
|
||||||
|
if(!($code === (new Totp)->GenerateToken(Base32::decode($secret)) || $user->use2faBackupCode((int) $code))) {
|
||||||
|
$this->flash("err", tr("error"), tr("incorrect_2fa_code"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$user = $request->getUser()->getChandlerUser();
|
$user = $request->getUser()->getChandlerUser();
|
||||||
$this->db->table("ChandlerTokens")->where("user", $user->getId())->delete(); #Logout from everywhere
|
$this->db->table("ChandlerTokens")->where("user", $user->getId())->delete(); #Logout from everywhere
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,9 @@ use openvk\Web\Models\Repositories\Albums;
|
||||||
use openvk\Web\Models\Repositories\Videos;
|
use openvk\Web\Models\Repositories\Videos;
|
||||||
use openvk\Web\Models\Repositories\Notes;
|
use openvk\Web\Models\Repositories\Notes;
|
||||||
use openvk\Web\Models\Repositories\Vouchers;
|
use openvk\Web\Models\Repositories\Vouchers;
|
||||||
|
use Chandler\Security\Authenticator;
|
||||||
|
use lfkeitel\phptotp\{Base32, Totp};
|
||||||
|
use chillerlan\QRCode\QRCode;
|
||||||
|
|
||||||
final class UserPresenter extends OpenVKPresenter
|
final class UserPresenter extends OpenVKPresenter
|
||||||
{
|
{
|
||||||
|
@ -283,6 +286,12 @@ final class UserPresenter extends OpenVKPresenter
|
||||||
if($_GET['act'] === "main" || $_GET['act'] == NULL) {
|
if($_GET['act'] === "main" || $_GET['act'] == NULL) {
|
||||||
if($this->postParam("old_pass") && $this->postParam("new_pass") && $this->postParam("repeat_pass")) {
|
if($this->postParam("old_pass") && $this->postParam("new_pass") && $this->postParam("repeat_pass")) {
|
||||||
if($this->postParam("new_pass") === $this->postParam("repeat_pass")) {
|
if($this->postParam("new_pass") === $this->postParam("repeat_pass")) {
|
||||||
|
if($this->user->identity->is2faEnabled()) {
|
||||||
|
$code = $this->postParam("code");
|
||||||
|
if(!($code === (new Totp)->GenerateToken(Base32::decode($this->user->identity->get2faSecret())) || $this->user->identity->use2faBackupCode((int) $code)))
|
||||||
|
$this->flashFail("err", tr("error"), tr("incorrect_2fa_code"));
|
||||||
|
}
|
||||||
|
|
||||||
if(!$this->user->identity->getChandlerUser()->updatePassword($this->postParam("new_pass"), $this->postParam("old_pass")))
|
if(!$this->user->identity->getChandlerUser()->updatePassword($this->postParam("new_pass"), $this->postParam("old_pass")))
|
||||||
$this->flashFail("err", tr("error"), tr("error_old_password"));
|
$this->flashFail("err", tr("error"), tr("error_old_password"));
|
||||||
} else {
|
} else {
|
||||||
|
@ -376,4 +385,64 @@ final class UserPresenter extends OpenVKPresenter
|
||||||
$this->template->user = $user;
|
$this->template->user = $user;
|
||||||
$this->template->themes = Themepacks::i()->getThemeList();
|
$this->template->themes = Themepacks::i()->getThemeList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderTwoFactorAuthSettings(): void
|
||||||
|
{
|
||||||
|
$this->assertUserLoggedIn();
|
||||||
|
|
||||||
|
if($this->user->identity->is2faEnabled()) {
|
||||||
|
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||||
|
if(!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash))
|
||||||
|
$this->flashFail("err", tr("error"), tr("incorrect_password"));
|
||||||
|
|
||||||
|
$this->user->identity->generate2faBackupCodes();
|
||||||
|
$this->template->_template = "User/TwoFactorAuthCodes.xml";
|
||||||
|
$this->template->codes = $this->user->identity->get2faBackupCodes();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->redirect("/settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret = Base32::encode(Totp::GenerateSecret(16));
|
||||||
|
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||||
|
$this->willExecuteWriteAction();
|
||||||
|
|
||||||
|
if(!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash))
|
||||||
|
$this->flashFail("err", tr("error"), tr("incorrect_password"));
|
||||||
|
|
||||||
|
$secret = $this->postParam("secret");
|
||||||
|
$code = $this->postParam("code");
|
||||||
|
|
||||||
|
if($code === (new Totp)->GenerateToken(Base32::decode($secret))) {
|
||||||
|
$this->user->identity->set2fa_secret($secret);
|
||||||
|
$this->user->identity->save();
|
||||||
|
|
||||||
|
$this->flash("succ", tr("two_factor_authentication_enabled"), tr("two_factor_authentication_enabled_description"));
|
||||||
|
$this->redirect("/settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->template->secret = $secret;
|
||||||
|
$this->flash("err", tr("error"), tr("incorrect_code"));
|
||||||
|
} else {
|
||||||
|
$this->template->secret = $secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
$issuer = OPENVK_ROOT_CONF["openvk"]["appearance"]["name"];
|
||||||
|
$email = $this->user->identity->getEmail();
|
||||||
|
$this->template->qrCode = substr((new QRCode)->render("otpauth://totp/$issuer:$email?secret=$secret&issuer=$issuer"), 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDisableTwoFactorAuth(): void
|
||||||
|
{
|
||||||
|
$this->assertUserLoggedIn();
|
||||||
|
$this->willExecuteWriteAction();
|
||||||
|
|
||||||
|
if(!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash))
|
||||||
|
$this->flashFail("err", tr("error"), tr("incorrect_password"));
|
||||||
|
|
||||||
|
$this->user->identity->set2fa_secret(NULL);
|
||||||
|
$this->user->identity->save();
|
||||||
|
$this->flashFail("succ", tr("information_-1"), tr("two_factor_authentication_disabled_message"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ use Chandler\Database\DatabaseConnection as DB;
|
||||||
use openvk\VKAPI\Exceptions\APIErrorException;
|
use openvk\VKAPI\Exceptions\APIErrorException;
|
||||||
use openvk\Web\Models\Entities\{User, APIToken};
|
use openvk\Web\Models\Entities\{User, APIToken};
|
||||||
use openvk\Web\Models\Repositories\{Users, APITokens};
|
use openvk\Web\Models\Repositories\{Users, APITokens};
|
||||||
|
use lfkeitel\phptotp\{Base32, Totp};
|
||||||
|
|
||||||
final class VKAPIPresenter extends OpenVKPresenter
|
final class VKAPIPresenter extends OpenVKPresenter
|
||||||
{
|
{
|
||||||
|
@ -161,6 +162,10 @@ final class VKAPIPresenter extends OpenVKPresenter
|
||||||
$uId = $chUser->related("profiles.user")->fetch()->id;
|
$uId = $chUser->related("profiles.user")->fetch()->id;
|
||||||
$user = (new Users)->get($uId);
|
$user = (new Users)->get($uId);
|
||||||
|
|
||||||
|
$code = $this->requestParam("code");
|
||||||
|
if($user->is2faEnabled() && !($code === (new Totp)->GenerateToken(Base32::decode($user->get2faSecret())) || $user->use2faBackupCode((int) $code)))
|
||||||
|
$this->fail(28, "Invalid 2FA code", "internal", "acquireToken");
|
||||||
|
|
||||||
$token = new APIToken;
|
$token = new APIToken;
|
||||||
$token->setUser($user);
|
$token->setUser($user);
|
||||||
$token->save();
|
$token->save();
|
||||||
|
|
|
@ -17,6 +17,11 @@
|
||||||
<label for="password">Новый пароль: </label>
|
<label for="password">Новый пароль: </label>
|
||||||
<input id="password" type="password" name="password" required />
|
<input id="password" type="password" name="password" required />
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
|
{if $is2faEnabled}
|
||||||
|
<label for="code">Код двухфакторной аутентификации: </label>
|
||||||
|
<input id="code" type="text" name="code" required />
|
||||||
|
<br/><br/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<input type="hidden" name="hash" value="{$csrfToken}" />
|
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||||
<input type="submit" value="Сбросить пароль" class="button" style="float: right;" />
|
<input type="submit" value="Сбросить пароль" class="button" style="float: right;" />
|
||||||
|
|
38
Web/Presenters/templates/Auth/LoginSecondFactor.xml
Normal file
38
Web/Presenters/templates/Auth/LoginSecondFactor.xml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{extends "../@layout.xml"}
|
||||||
|
{block title}{_"log_in"}{/block}
|
||||||
|
|
||||||
|
{block header}
|
||||||
|
{_"log_in"}
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block content}
|
||||||
|
<p>
|
||||||
|
{_"two_factor_authentication_login"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
<table cellspacing="7" cellpadding="0" width="40%" border="0" align="center">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span>{_code}: </span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="code" required />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="login" value="{$login}" />
|
||||||
|
<input type="hidden" name="password" value="{$password}" />
|
||||||
|
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||||
|
<input type="submit" value="{_'log_in'}" class="button" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
{/block}
|
|
@ -59,6 +59,14 @@
|
||||||
<input type="password" name="repeat_pass" style="width: 100%;" />
|
<input type="password" name="repeat_pass" style="width: 100%;" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr n:if="$user->is2faEnabled()">
|
||||||
|
<td width="120" valign="top">
|
||||||
|
<span class="nobold">{_"2fa_code"}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="code" style="width: 100%;" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
|
@ -71,6 +79,70 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<br/>
|
<br/>
|
||||||
|
<h4>{_two_factor_authentication}</h4>
|
||||||
|
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
|
||||||
|
<tbody>
|
||||||
|
{if $user->is2faEnabled()}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="accent-box">
|
||||||
|
{_two_factor_authentication_enabled}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
<a class="button" href="javascript:viewBackupCodes()">{_view_backup_codes}</a>
|
||||||
|
<a class="button" href="javascript:disableTwoFactorAuth()">{_disable}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function viewBackupCodes() {
|
||||||
|
MessageBox("Просмотр резервных кодов", `
|
||||||
|
<form id="back-codes-view-form" method="post" action="/settings/2fa">
|
||||||
|
<label for="password">Пароль</label>
|
||||||
|
<input type="password" id="password" name="password" required />
|
||||||
|
<input type="hidden" name="hash" value={$csrfToken} />
|
||||||
|
</form>
|
||||||
|
`, ["Просмотреть", "Отменить"], [
|
||||||
|
() => {
|
||||||
|
document.querySelector("#back-codes-view-form").submit();
|
||||||
|
}, Function.noop
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableTwoFactorAuth() {
|
||||||
|
MessageBox("Отключить 2FA", `
|
||||||
|
<form id="two-factor-auth-disable-form" method="post" action="/settings/2fa/disable">
|
||||||
|
<label for="password">Пароль</label>
|
||||||
|
<input type="password" id="password" name="password" required />
|
||||||
|
<input type="hidden" name="hash" value={$csrfToken} />
|
||||||
|
</form>
|
||||||
|
`, ["Отключить", "Отменить"], [
|
||||||
|
() => {
|
||||||
|
document.querySelector("#two-factor-auth-disable-form").submit();
|
||||||
|
}, Function.noop
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{else}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="accent-box">
|
||||||
|
{_two_factor_authentication_disabled}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
<a class="button" href="/settings/2fa">{_connect}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br/>
|
||||||
<h4>{_your_email_address}</h4>
|
<h4>{_your_email_address}</h4>
|
||||||
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
|
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -392,6 +464,17 @@
|
||||||
<span class="nobold">{_my_videos}</span>
|
<span class="nobold">{_my_videos}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="120" valign="top" align="right">
|
||||||
|
<input
|
||||||
|
n:attr="checked => $user->getLeftMenuItemStatus('audios')"
|
||||||
|
type="checkbox"
|
||||||
|
name="menu_audioj" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="nobold">{_my_audios}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="120" valign="top" align="right">
|
<td width="120" valign="top" align="right">
|
||||||
<input
|
<input
|
||||||
|
|
18
Web/Presenters/templates/User/TwoFactorAuthCodes.xml
Normal file
18
Web/Presenters/templates/User/TwoFactorAuthCodes.xml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{extends "../@layout.xml"}
|
||||||
|
{block title}{_"my_settings"} - {_"two_factor_authentication"}{/block}
|
||||||
|
|
||||||
|
{block header}
|
||||||
|
<a href="/settings">{_"my_settings"}</a> » {_"two_factor_authentication"}
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block content}
|
||||||
|
<h4>{_"backup_codes"}</h4>
|
||||||
|
<p>{_"two_factor_authentication_backup_codes_1"}</p>
|
||||||
|
<p>{_"two_factor_authentication_backup_codes_2"|noescape}</p>
|
||||||
|
|
||||||
|
<ol style="columns: 2; text-align: center;">
|
||||||
|
<li n:foreach="$codes as $code">{$code}</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p>{_"two_factor_authentication_backup_codes_3"}</p>
|
||||||
|
{/block}
|
48
Web/Presenters/templates/User/TwoFactorAuthSettings.xml
Normal file
48
Web/Presenters/templates/User/TwoFactorAuthSettings.xml
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{extends "../@layout.xml"}
|
||||||
|
{block title}{_"my_settings"} - {_"two_factor_authentication"}{/block}
|
||||||
|
|
||||||
|
{block header}
|
||||||
|
<a href="/settings">{_"my_settings"}</a> » {_"two_factor_authentication"}
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block content}
|
||||||
|
{_"two_factor_authentication_settings_1"|noescape}
|
||||||
|
<p>{_"two_factor_authentication_settings_2"}</p>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<img src="data:image/png;base64,{$qrCode}">
|
||||||
|
</div>
|
||||||
|
<p>{tr("two_factor_authentication_settings_3", $secret)|noescape}</p>
|
||||||
|
<p>{_"two_factor_authentication_settings_4"}</p>
|
||||||
|
<form method="POST">
|
||||||
|
<table cellspacing="7" cellpadding="0" width="40%" border="0" align="center">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span>{_code}: </span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="code" required />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span>{_password}: </span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="password" name="password" required />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="secret" value="{$secret}" />
|
||||||
|
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||||
|
<input type="submit" value="{_enable}" class="button" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
{/block}
|
|
@ -51,6 +51,10 @@ routes:
|
||||||
handler: "Auth->su"
|
handler: "Auth->su"
|
||||||
- url: "/settings"
|
- url: "/settings"
|
||||||
handler: "User->settings"
|
handler: "User->settings"
|
||||||
|
- url: "/settings/2fa"
|
||||||
|
handler: "User->twoFactorAuthSettings"
|
||||||
|
- url: "/settings/2fa/disable"
|
||||||
|
handler: "User->disableTwoFactorAuth"
|
||||||
- url: "/id{num}"
|
- url: "/id{num}"
|
||||||
handler: "User->view"
|
handler: "User->view"
|
||||||
- url: "/friends{num}"
|
- url: "/friends{num}"
|
||||||
|
|
|
@ -1575,3 +1575,10 @@ body.scrolled .toTop:hover {
|
||||||
border-top: 3px solid rgb(130, 130, 130);
|
border-top: 3px solid rgb(130, 130, 130);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accent-box {
|
||||||
|
background-color: white;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px;
|
||||||
|
border: 1px solid #C0CAD5;
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,9 @@
|
||||||
"netcarver/textile": "^3.7@dev",
|
"netcarver/textile": "^3.7@dev",
|
||||||
"al/emoji-detector": "dev-master",
|
"al/emoji-detector": "dev-master",
|
||||||
"ezyang/htmlpurifier": "dev-master",
|
"ezyang/htmlpurifier": "dev-master",
|
||||||
"scssphp/scssphp": "dev-master"
|
"scssphp/scssphp": "dev-master",
|
||||||
|
"lfkeitel/phptotp": "dev-master",
|
||||||
|
"chillerlan/php-qrcode": "dev-main"
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev"
|
"minimum-stability": "dev"
|
||||||
}
|
}
|
||||||
|
|
11
install/sqls/00013-2fa.sql
Normal file
11
install/sqls/00013-2fa.sql
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
ALTER TABLE `profiles` ADD COLUMN `2fa_secret` VARCHAR(26);
|
||||||
|
ALTER TABLE `groups` ADD COLUMN `2fa_required` BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `2fa_backup_codes` (
|
||||||
|
`owner` bigint(20) unsigned NOT NULL,
|
||||||
|
`code` int unsigned NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `2fa_backup_codes`
|
||||||
|
ADD KEY `code` (`code`,`owner`),
|
||||||
|
ADD KEY `FK_ownerToCode` (`owner`);
|
|
@ -357,6 +357,37 @@
|
||||||
"ui_settings_rating_show" = "Show";
|
"ui_settings_rating_show" = "Show";
|
||||||
"ui_settings_rating_hide" = "Hide";
|
"ui_settings_rating_hide" = "Hide";
|
||||||
|
|
||||||
|
/* Two-factor authentication */
|
||||||
|
|
||||||
|
"two_factor_authentication" = "Two-factor authentication";
|
||||||
|
"two_factor_authentication_disabled" = "Provides reliable protection against hacking: to enter the page, you must enter the code obtained in the 2FA application.";
|
||||||
|
"two_factor_authentication_enabled" = "Two-factor authentication is enabled. Your page is protected.";
|
||||||
|
"two_factor_authentication_login" = "You have two-factor authentication enabled. To login, enter the code received in the application.";
|
||||||
|
|
||||||
|
"two_factor_authentication_settings_1" = "Two-factor authentication via TOTP can be used even without internet. To do this, you need a code generation app. For example, <b>Google Authenticator</b> for Android and iOS or <b>FOSS Aegis and andOTP</b> for Android.";
|
||||||
|
"two_factor_authentication_settings_2" = "Using the app for two-factor authentication, scan the QR code below:";
|
||||||
|
"two_factor_authentication_settings_3" = "or manually enter the given secret key: <b>$1</b>.";
|
||||||
|
"two_factor_authentication_settings_4" = "Now enter the code that the application gave you and the password for your page so that we can confirm that you really are.";
|
||||||
|
|
||||||
|
"connect" = "Connect";
|
||||||
|
"enable" = "Enable";
|
||||||
|
"disable" = "Disable";
|
||||||
|
"code" = "Code";
|
||||||
|
"2fa_code" = "2FA code";
|
||||||
|
|
||||||
|
"incorrect_password" = "Incorrect password";
|
||||||
|
"incorrect_code" = "Incorrect code";
|
||||||
|
"incorrect_2fa_code" = "Incorrect two-factor authentication code";
|
||||||
|
"two_factor_authentication_enabled_message" = "Two-factor authentication enabled";
|
||||||
|
"two_factor_authentication_enabled_message_description" = "Your page has become more difficult to hack. We recommend that you download <a href='javascript:viewBackupCodes()'>backup codes</a>";
|
||||||
|
"two_factor_authentication_disabled_message" = "Two-factor authentication disabled";
|
||||||
|
|
||||||
|
"view_backup_codes" = "View backup codes";
|
||||||
|
"backup_codes" = "Backup codes for login confirmation";
|
||||||
|
"two_factor_authentication_backup_codes_1" = "Backup codes allow you to validate your login when you don't have access to your phone, for example, while traveling.";
|
||||||
|
"two_factor_authentication_backup_codes_2" = "You have <b>10 more codes</b>, each code can only be used once. Print them out, put them away in a safe place and use them when you need codes to validate your login.";
|
||||||
|
"two_factor_authentication_backup_codes_3" = "You can get new codes if they run out. Only the last created backup codes are valid.";
|
||||||
|
|
||||||
/* Sorting */
|
/* Sorting */
|
||||||
|
|
||||||
"sort_randomly" = "Sort randomly";
|
"sort_randomly" = "Sort randomly";
|
||||||
|
|
|
@ -387,6 +387,35 @@
|
||||||
"ui_settings_rating_show" = "Показывать";
|
"ui_settings_rating_show" = "Показывать";
|
||||||
"ui_settings_rating_hide" = "Скрывать";
|
"ui_settings_rating_hide" = "Скрывать";
|
||||||
|
|
||||||
|
"two_factor_authentication" = "Двухфакторная аутентификация";
|
||||||
|
"two_factor_authentication_disabled" = "Обеспечивает надежную защиту от взлома: для входа на страницу необходимо ввести код, полученный в приложении 2FA.";
|
||||||
|
"two_factor_authentication_enabled" = "Двухфакторная аутентификация включена. Ваша страница защищена.";
|
||||||
|
"two_factor_authentication_login" = "У вас включена двухфакторная аутентификация. Для входа введите код полученный в приложении.";
|
||||||
|
|
||||||
|
"two_factor_authentication_settings_1" = "Двухфакторную аутентификацию через TOTP можно использовать даже без интернета. Для этого вам понадобится приложение для генерации кодов. Например, <b>Google Authenticator</b> для Android и iOS или свободные <b>Aegis и andOTP</b> для Android.";
|
||||||
|
"two_factor_authentication_settings_2" = "Используя приложение для двухфакторной аутентификации, отсканируйте приведенный ниже QR-код:";
|
||||||
|
"two_factor_authentication_settings_3" = "или вручную введите секретный ключ: <b>$1</b>.";
|
||||||
|
"two_factor_authentication_settings_4" = "Теперь введите код, который вам предоставило приложение, и пароль от вашей страницы, чтобы мы могли подтвердить, что вы действительно вы.";
|
||||||
|
|
||||||
|
"connect" = "Подключить";
|
||||||
|
"enable" = "Включить";
|
||||||
|
"disable" = "Отключить";
|
||||||
|
"code" = "Код";
|
||||||
|
"2fa_code" = "Код 2FA";
|
||||||
|
|
||||||
|
"incorrect_password" = "Неверный пароль";
|
||||||
|
"incorrect_code" = "Неверный код";
|
||||||
|
"incorrect_2fa_code" = "Неверный код двухфакторной аутентификации";
|
||||||
|
"two_factor_authentication_enabled_message" = "Двухфакторная аутентификация включена";
|
||||||
|
"two_factor_authentication_enabled_message_description" = "Вашу страницу стало труднее взломать. Рекомендуем вам скачать <a href='javascript:viewBackupCodes()'>резервные коды</a>";
|
||||||
|
"two_factor_authentication_disabled_message" = "Двухфакторная аутентификация отключена";
|
||||||
|
|
||||||
|
"view_backup_codes" = "Посмотреть резервные коды";
|
||||||
|
"backup_codes" = "Резервные коды для подтверждения входа";
|
||||||
|
"two_factor_authentication_backup_codes_1" = "Резервные коды позволяют подтверждать вход, когда у вас нет доступа к телефону, например, в путешествии.";
|
||||||
|
"two_factor_authentication_backup_codes_2" = "У вас есть ещё <b>10 кодов</b>, каждым кодом можно воспользоваться только один раз. Распечатайте их, уберите в надежное место и используйте, когда потребуются коды для подтверждения входа.";
|
||||||
|
"two_factor_authentication_backup_codes_3" = "Вы можете получить новые коды, если они заканчиваются. Действительны только последние созданные резервные коды.";
|
||||||
|
|
||||||
/* Sorting */
|
/* Sorting */
|
||||||
|
|
||||||
"sort_randomly" = "Сортировать рандомно";
|
"sort_randomly" = "Сортировать рандомно";
|
||||||
|
|
Loading…
Reference in a new issue