Skip to content

Commit

Permalink
CAPTCHA for TOTP
Browse files Browse the repository at this point in the history
  • Loading branch information
CatoTH committed Sep 8, 2024
1 parent cf2d7a8 commit 6c3e67f
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 43 deletions.
27 changes: 13 additions & 14 deletions components/SecondFactorAuthentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class SecondFactorAuthentication
private const SESSION_KEY_2FA_SETUP_KEY = 'settingUp2FAKey';
private const SESSION_KEY_2FA_ONGOING = 'loggedIn2FAInProgress';
private const SESSION_KEY_2FA_REGISTRATION_ONGOING = 'loggedInForced2FARegistrationInProgress';
private const TIMEOUT_2FA_SESSION = 300;
public const TIMEOUT_2FA_SESSION = 300;

public function __construct(Session $session) {
$this->session = $session;
Expand Down Expand Up @@ -93,7 +93,8 @@ public function attemptRegisteringSecondFactor(User $user, string $secondFactor)
throw new \RuntimeException(\Yii::t('user', 'err_2fa_nosession_user'));
}
if ($data['time'] < time() - self::TIMEOUT_2FA_SESSION) {
$msg = \Yii::t('user', 'err_2fa_timeout');
$minutes = SecondFactorAuthentication::TIMEOUT_2FA_SESSION / 60;
$msg = str_replace('%minutes%', (string) $minutes, \Yii::t('user', 'err_2fa_timeout'));
throw new \RuntimeException(str_replace('%seconds%', (string)self::TIMEOUT_2FA_SESSION, $msg));
}
if (!$secondFactor) {
Expand Down Expand Up @@ -175,25 +176,22 @@ private function initForcedSecondFactorSetting(User $user): ResponseInterface
return new RedirectResponse(UrlHelper::createUrl('/user/login2fa-force-registration'));
}

public function hasOngoingSession(): bool
public function getOngoingSessionUser(): ?User
{
$data = $this->session->get(self::SESSION_KEY_2FA_ONGOING);
if (!$data) {
return false;
return null;
}
if ($data['time'] < time() - self::TIMEOUT_2FA_SESSION) {
return false;
return null;
}
return true;

return User::findOne(['id' => $data['user_id']]);
}

public function confirmLoginWithSecondFactor(string $secondFactor): ?User
{
if (!$this->hasOngoingSession()) {
return null;
}
$data = $this->session->get(self::SESSION_KEY_2FA_ONGOING);
$user = User::findOne(['id' => $data['user_id']]);
$user = $this->getOngoingSessionUser();
if (!$user) {
return null;
}
Expand All @@ -202,7 +200,7 @@ public function confirmLoginWithSecondFactor(string $secondFactor): ?User
if (!$userSettings->secondFactorKeys) {
return null;
}
foreach ($userSettings->secondFactorKeys as $index => $key) {
foreach ($userSettings->secondFactorKeys as $key) {
$totp = TOTP::createFromSecret($key['secret']);
if ($this->checkOtp($totp, $secondFactor)) {
return $user;
Expand All @@ -212,7 +210,7 @@ public function confirmLoginWithSecondFactor(string $secondFactor): ?User
return null;
}

private function getForcedRegistrationUser(): User
public function getForcedRegistrationUser(): User
{
$data = $this->session->get(self::SESSION_KEY_2FA_REGISTRATION_ONGOING);
if (!$data) {
Expand Down Expand Up @@ -249,7 +247,8 @@ public function attemptForcedRegisteringSecondFactor(string $secondFactor): User
throw new \RuntimeException(\Yii::t('user', 'err_2fa_nosession_user'));
}
if ($data['time'] < time() - self::TIMEOUT_2FA_SESSION) {
$msg = \Yii::t('user', 'err_2fa_timeout');
$minutes = SecondFactorAuthentication::TIMEOUT_2FA_SESSION / 60;
$msg = str_replace('%minutes%', (string) $minutes, \Yii::t('user', 'err_2fa_timeout'));
throw new \RuntimeException(str_replace('%seconds%', (string)self::TIMEOUT_2FA_SESSION, $msg));
}
if (!$secondFactor) {
Expand Down
65 changes: 50 additions & 15 deletions controllers/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,12 @@ public function actionLogin(string $backUrl = ''): ResponseInterface

public function actionLogin2fa(string $backUrl = ''): ResponseInterface
{
if (!$this->secondFactorAuthentication->hasOngoingSession()) {
$this->getHttpSession()->setFlash('error', 'Die Zeit für die Eingabe des Codes ist abgelaufen');
$loggingInUser = $this->secondFactorAuthentication->getOngoingSessionUser();
if (!$loggingInUser) {
die("!");
$minutes = SecondFactorAuthentication::TIMEOUT_2FA_SESSION / 60;

Check failure on line 127 in controllers/UserController.php

View workflow job for this annotation

GitHub Actions / evaluate-pr

Unreachable statement - code above always terminates.
$msg = str_replace('%minutes%', (string) $minutes, \Yii::t('user', 'err_2fa_timeout'));
$this->getHttpSession()->setFlash('error', $msg);

return new RedirectResponse(UrlHelper::createUrl('/user/login'));
}
Expand All @@ -133,29 +137,58 @@ public function actionLogin2fa(string $backUrl = ''): ResponseInterface

$error = null;
if ($this->isPostSet('2fa') && trim($this->getPostValue('2fa'))) {
$successUser = $this->secondFactorAuthentication->confirmLoginWithSecondFactor($this->getPostValue('2fa'));
if ($successUser) {
$this->loginUser($successUser);
$this->getHttpSession()->setFlash('success', \Yii::t('user', 'welcome'));
if (Captcha::needsCaptcha($loggingInUser->email) && !Captcha::checkEnteredCaptcha($this->getRequestValue('captcha'))) {
$error = \Yii::t('user', 'login_err_captcha');
goto loginForm;
}

return new RedirectResponse($backUrl);
} else {
$error = 'Ungültiger Code.';
$successUser = $this->secondFactorAuthentication->confirmLoginWithSecondFactor($this->getPostValue('2fa'));
if (!$successUser) {
FailedLoginAttempt::logFailedAttempt($loggingInUser->email);
$error = \Yii::t('user', 'err_2fa_incorrect');
goto loginForm;
}

$this->loginUser($successUser);
$this->getHttpSession()->setFlash('success', \Yii::t('user', 'welcome'));

return new RedirectResponse($backUrl);
}
// @TODO CAPTCHA

return new HtmlResponse($this->render('login-2fa', ['error' => $error]));
$this->secondFactorAuthentication->getOngoingSessionUser();

loginForm:
$resp = new HtmlResponse($this->render('login-2fa', [
'captchaUsername' => $loggingInUser->email,
'error' => $error,
]));

$this->secondFactorAuthentication->getOngoingSessionUser();

return $resp;
}

public function actionLogin2faForceRegistration(string $backUrl = ''): ResponseInterface
{
try {
$loggingInUser = $this->secondFactorAuthentication->getForcedRegistrationUser();
} catch (\Exception $e) {
$this->getHttpSession()->setFlash('error', $e->getMessage());

return new RedirectResponse(UrlHelper::createUrl('/user/login'));
}

if ($backUrl === '') {
$backUrl = '/';
}

$error = null;
if ($this->isPostSet('set2fa') && trim($this->getPostValue('set2fa'))) {
if (Captcha::needsCaptcha($loggingInUser->email) && !Captcha::checkEnteredCaptcha($this->getRequestValue('captcha'))) {
$error = \Yii::t('user', 'login_err_captcha');
goto loginForm;
}

try {
$successUser = $this->secondFactorAuthentication->attemptForcedRegisteringSecondFactor(trim($this->getPostValue('set2fa')));
$this->loginUser($successUser);
Expand All @@ -166,11 +199,13 @@ public function actionLogin2faForceRegistration(string $backUrl = ''): ResponseI
$error = $e->getMessage();
}
}
// @TODO CAPTCHA

$addSecondFactorKey = $this->secondFactorAuthentication->createForcedRegistrationSecondFactor();

return new HtmlResponse($this->render('login-2fa-force-registration', ['error' => $error, 'addSecondFactorKey' => $addSecondFactorKey]));
loginForm:
return new HtmlResponse($this->render('login-2fa-force-registration', [
'error' => $error,
'captchaUsername' => $loggingInUser->email,
'addSecondFactorKey' => $this->secondFactorAuthentication->createForcedRegistrationSecondFactor(),
]));
}

public function actionToken(): JsonResponse
Expand Down
2 changes: 1 addition & 1 deletion messages/de/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
'bitte fordere eine neue Änderung an und rufe den Link innerhalb von 24 Stunden auf',
'err_2fa_nosession_user' => 'Keine aktiver Zwei-Faktoren-Anmeldungs-Vorgang für diese Benutzer*in gefunden',
'err_2fa_nosession' => 'Kein aktiver Anmeldungsvorgang',
'err_2fa_timeout' => 'Bitte bestätige die Anmeldung innerhalb von %seconds% Sekunden.',
'err_2fa_timeout' => 'Bitte bestätige die Anmeldung innerhalb von %minutes% Minuten.',
'err_2fa_empty' => 'Kein Code eingegeben',
'err_2fa_incorrect' => 'Ungültiger Code eingegeben',
'err_2fa_nocode' => 'Keine Zwei-Faktoren-Anmeldung eingerichtet',
Expand Down
2 changes: 1 addition & 1 deletion messages/en/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
'err_change_toolong' => 'The request is too old; please request another change request and confirm the e-mail within 24 hours',
'err_2fa_nosession_user' => 'No ongoing TOTP registration for the current user found',
'err_2fa_nosession' => 'No login session ongoing',
'err_2fa_timeout' => 'Please confirm the second factor within %seconds% seconds.',
'err_2fa_timeout' => 'Please confirm the second factor within %minutes% minutes.',
'err_2fa_empty' => 'Empty code given',
'err_2fa_incorrect' => 'Incorrect code provided',
'err_2fa_nocode' => 'No second factor registered',
Expand Down
23 changes: 17 additions & 6 deletions views/user/login-2fa-force-registration.php
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
<?php

use app\models\db\ConsultationText;
use app\models\settings\AntragsgruenApp;
use app\components\{Captcha, UrlHelper};
use app\models\db\User;
use app\models\forms\LoginUsernamePasswordForm;
use app\components\Captcha;
use OTPHP\TOTP;
use yii\helpers\Html;
use app\models\settings\Site as SiteSettings;

/**
* @var yii\web\View $this
* @var string|null $error
* @var TOTP|null $addSecondFactorKey
* @var string $captchaUsername
*/

/** @var \app\controllers\UserController $controller */
Expand Down Expand Up @@ -56,6 +52,21 @@
<input type="text" name="set2fa" class="form-control">
</label>
</div>

<?php
if (Captcha::needsCaptcha($captchaUsername)) {
$image = Captcha::createInlineCaptcha();
?>
<label for="captchaInput"><?= Yii::t('user', 'login_captcha') ?>:</label><br>
<div class="captchaHolder">
<img src="<?= $image ?>" alt="" width="150">
<input type="text" value="" autocomplete="off" name="captcha" id="captchaInput" class="form-control" required>
</div>
<br><br>
<?php
}
?>

<div class="saveRow">
<button type="submit" class="btn btn-success"><?= Yii::t('user', 'login_btn_login') ?></button>
</div>
Expand Down
22 changes: 16 additions & 6 deletions views/user/login-2fa.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
<?php

use app\models\db\ConsultationText;
use app\models\settings\AntragsgruenApp;
use app\components\{Captcha, UrlHelper};
use app\models\db\User;
use app\models\forms\LoginUsernamePasswordForm;
use app\components\Captcha;
use yii\helpers\Html;
use app\models\settings\Site as SiteSettings;

/**
* @var yii\web\View $this
* @var string|null $error
* @var string $captchaUsername
*/

/** @var \app\controllers\UserController $controller */
Expand Down Expand Up @@ -45,6 +41,20 @@
<input type="text" name="2fa" class="form-control" id="2facode">
</div>

<?php
if (Captcha::needsCaptcha($captchaUsername)) {
$image = Captcha::createInlineCaptcha();
?>
<label for="captchaInput"><?= Yii::t('user', 'login_captcha') ?>:</label><br>
<div class="captchaHolder">
<img src="<?= $image ?>" alt="" width="150">
<input type="text" value="" autocomplete="off" name="captcha" id="captchaInput" class="form-control" required>
</div>
<br><br>
<?php
}
?>

<button type="submit" class="btn btn-success"><?= Yii::t('user', 'login_btn_login') ?></button>
</div>

Expand Down

0 comments on commit 6c3e67f

Please sign in to comment.