From dd2eefa50f4779bab9e7d56aa5c1242d5f1766b6 Mon Sep 17 00:00:00 2001 From: Jan Schuppik Date: Thu, 24 Jul 2025 21:09:08 +0200 Subject: [PATCH] Add: 2FA login challenge without proper validation --- .../controllers/AuthenticationController.php | 16 +++-- .../forms/Authentication/Challenge2FAForm.php | 71 +++++++++++++++++++ .../forms/Authentication/LoginForm.php | 17 +++-- library/Icinga/Authentication/Auth.php | 17 +++-- library/Icinga/User.php | 61 ++++++++++++++++ 5 files changed, 163 insertions(+), 19 deletions(-) create mode 100644 application/forms/Authentication/Challenge2FAForm.php diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php index a8b88b803..07b2d9282 100644 --- a/application/controllers/AuthenticationController.php +++ b/application/controllers/AuthenticationController.php @@ -8,10 +8,12 @@ use Icinga\Application\Icinga; use Icinga\Application\Logger; use Icinga\Common\Database; use Icinga\Exception\AuthenticationException; +use Icinga\Forms\Authentication\Challenge2FAForm; use Icinga\Forms\Authentication\LoginForm; use Icinga\Web\Controller; use Icinga\Web\Helper\CookieHelper; use Icinga\Web\RememberMe; +use Icinga\Web\Session; use Icinga\Web\Url; use RuntimeException; @@ -41,7 +43,14 @@ class AuthenticationController extends Controller if (($requiresSetup = $icinga->requiresSetup()) && $icinga->setupTokenExists()) { $this->redirectNow(Url::fromPath('setup')); } - $form = new LoginForm(); + + $user = $this->Auth()->getUser(); + $form = ($user !== null + && $user->getTwoFactorEnabled() + && Session::getSession()->get('must_challenge_2fa_token', false) === true) + ? new Challenge2FAForm() + : new LoginForm(); + if (RememberMe::hasCookie() && $this->hasDb()) { $authenticated = false; @@ -91,13 +100,8 @@ class AuthenticationController extends Controller ->sendResponse(); exit; } - // FORM DOES NOT REDIRECT, IF USER HAS 2FA ENABLED and token hasn't been challenged $form->handleRequest(); } -// if ($user->has2FA() && irgendwas_mit_session()) { -// // 2 FA form erstellen und zeigen und handeln - // in der session speichern ob der token gepasst hat -// } $this->view->form = $form; $this->view->defaultTitle = $this->translate('Icinga Web 2 Login'); $this->view->requiresSetup = $requiresSetup; diff --git a/application/forms/Authentication/Challenge2FAForm.php b/application/forms/Authentication/Challenge2FAForm.php new file mode 100644 index 000000000..3bb99820e --- /dev/null +++ b/application/forms/Authentication/Challenge2FAForm.php @@ -0,0 +1,71 @@ +setRequiredCue(null); + $this->setName('form_challenge_2fa'); + $this->setSubmitLabel($this->translate('Verify')); + $this->setProgressLabel($this->translate('Verifying')); + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData) + { + $this->addElement( + 'text', + 'code', + [ + 'autocapitalize' => 'off', + 'class' => 'autofocus content-centered', + 'placeholder' => $this->translate('Please enter your 2FA code'), + 'required' => true, + 'autocomplete' => 'off', + + ] + ); + + $this->addElement( + 'hidden', + 'redirect', + [ + 'value' => Url::fromRequest()->getParam('redirect') + ] + ); + } + + public function onSuccess() + { + // TODO: Implement proper 2FA code validation + if ($_POST['code'] == 666) { + + $auth = Auth::getInstance(); + $user = $auth->getUser(); + Session::getSession()->set('challenged_successful_2fa_token', true); + Session::getSession()->delete('must_challenge_2fa_token'); + + $auth->setAuthenticated($user); + + AuthenticationHook::triggerLogin($user); + $this->getResponse()->setRerenderLayout(true); + return true; + } + + $this->getElement('code')->addError($this->translate('Code is invalid!')); + + return false; + } +} diff --git a/application/forms/Authentication/LoginForm.php b/application/forms/Authentication/LoginForm.php index b27c5c799..ab4e3c82b 100644 --- a/application/forms/Authentication/LoginForm.php +++ b/application/forms/Authentication/LoginForm.php @@ -14,6 +14,7 @@ use Icinga\Exception\Http\HttpBadRequestException; use Icinga\User; use Icinga\Web\Form; use Icinga\Web\RememberMe; +use Icinga\Web\Session; use Icinga\Web\Url; /** @@ -164,14 +165,16 @@ class LoginForm extends Form // If user has 2FA enabled and the token hasn't been validated, redirect to login again, so that // the token is challenged. - $redirect = $this->getElement('redirect'); - $old = $redirect->getValue(); - $new = []; - if ($old) { - $new['redirect'] = $old; + if ($user->getTwoFactorEnabled() && ! $user->getTwoFactorSuccessful()) { + $redirect = $this->getElement('redirect'); + $redirect->setValue( + Url::fromPath('authentication/login', + ['redirect' => $redirect->getValue()])->getRelativeUrl() + ); + Session::getSession()->set('must_challenge_2fa_token', true); + + return true; } - $redirect->setValue(Url::fromPath('authentication/login', $new)->getRelativeUrl()); - return true; $this->getResponse()->setRerenderLayout(true); return true; diff --git a/library/Icinga/Authentication/Auth.php b/library/Icinga/Authentication/Auth.php index 5b30a03e8..e36f797b9 100644 --- a/library/Icinga/Authentication/Auth.php +++ b/library/Icinga/Authentication/Auth.php @@ -87,10 +87,10 @@ class Auth */ public function isAuthenticated() { - // return false just for testing. isAuthenticated must return false if the user is authentiacted but has 2FA enabled and the token hasn't been challenged yet. - return false; - if ($this->user !== null) { + if ($this->user->getTwoFactorEnabled() && ! $this->user->getTwoFactorSuccessful()) { + return false; + } return true; } $this->authenticateFromSession(); @@ -98,7 +98,8 @@ class Auth return false; } - // real 2fa check from above must happen here + // 2fa check from must happen here, to apply the 2fa challenge for external users as well + // but the session authentication would also get the 2fa challenge return true; } @@ -136,7 +137,9 @@ class Auth } // don't log if 2fa hasn't been challenged yet - AuditHook::logActivity('login', 'User logged in'); + if (!$user->getTwoFactorEnabled() || $user->getTwoFactorSuccessful()) { + AuditHook::logActivity('login', 'User logged in'); + } } /** @@ -457,6 +460,8 @@ class Auth $admissionLoader = new AdmissionLoader(); $admissionLoader->applyRoles($user); - // Set 2FA status from the user preferences in the user obect + // Set 2FA status from the user preferences & session in the user object + $user->setTwoFactorEnabled($preferences->getValue('icingaweb', 'enabled_2fa') == 1); + $user->setTwoFactorSuccessful(Session::getSession()->get('challenged_successful_2fa_token', false)); } } diff --git a/library/Icinga/User.php b/library/Icinga/User.php index 8610dd0fe..9d4236366 100644 --- a/library/Icinga/User.php +++ b/library/Icinga/User.php @@ -122,6 +122,20 @@ class User */ protected $isHttpUser = false; + /** + * Whether the user has 2FA enabled + * + * @var bool + */ + protected $twoFactorEnabled = false; + + /** + * Whether the user has successfully completed 2FA + * + * @var bool + */ + protected $twoFactorSuccessful = false; + /** * Creates a user object given the provided information * @@ -646,4 +660,51 @@ class User return $navigation; } + + /** + * Get whether the user has 2FA enabled + * + * @return bool + */ + public function getTwoFactorEnabled(): bool + { + return $this->twoFactorEnabled; + } + + /** + * Set whether the user has 2FA enabled + * + * @param bool $enabled + * + * @return $this + */ + public function setTwoFactorEnabled(bool $enabled) + { + $this->twoFactorEnabled = $enabled; + + return $this; + } + + /** + * Get whether the user has successfully completed 2FA + * + * @return bool + */ + public function getTwoFactorSuccessful(): bool + { + return $this->twoFactorSuccessful; + } + + /** + * Set whether the user has successfully completed 2FA + * + * @param bool $successful + * + * @return $this + */ + public function setTwoFactorSuccessful(bool $successful): self + { + $this->twoFactorSuccessful = $successful; + return $this; + } }