Add: 2FA login challenge without proper validation

This commit is contained in:
Jan Schuppik 2025-07-24 21:09:08 +02:00
parent a1d36202dc
commit dd2eefa50f
5 changed files with 163 additions and 19 deletions

View File

@ -8,10 +8,12 @@ use Icinga\Application\Icinga;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Common\Database; use Icinga\Common\Database;
use Icinga\Exception\AuthenticationException; use Icinga\Exception\AuthenticationException;
use Icinga\Forms\Authentication\Challenge2FAForm;
use Icinga\Forms\Authentication\LoginForm; use Icinga\Forms\Authentication\LoginForm;
use Icinga\Web\Controller; use Icinga\Web\Controller;
use Icinga\Web\Helper\CookieHelper; use Icinga\Web\Helper\CookieHelper;
use Icinga\Web\RememberMe; use Icinga\Web\RememberMe;
use Icinga\Web\Session;
use Icinga\Web\Url; use Icinga\Web\Url;
use RuntimeException; use RuntimeException;
@ -41,7 +43,14 @@ class AuthenticationController extends Controller
if (($requiresSetup = $icinga->requiresSetup()) && $icinga->setupTokenExists()) { if (($requiresSetup = $icinga->requiresSetup()) && $icinga->setupTokenExists()) {
$this->redirectNow(Url::fromPath('setup')); $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()) { if (RememberMe::hasCookie() && $this->hasDb()) {
$authenticated = false; $authenticated = false;
@ -91,13 +100,8 @@ class AuthenticationController extends Controller
->sendResponse(); ->sendResponse();
exit; exit;
} }
// FORM DOES NOT REDIRECT, IF USER HAS 2FA ENABLED and token hasn't been challenged
$form->handleRequest(); $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->form = $form;
$this->view->defaultTitle = $this->translate('Icinga Web 2 Login'); $this->view->defaultTitle = $this->translate('Icinga Web 2 Login');
$this->view->requiresSetup = $requiresSetup; $this->view->requiresSetup = $requiresSetup;

View File

@ -0,0 +1,71 @@
<?php
namespace Icinga\Forms\Authentication;
use Icinga\Application\Hook\AuthenticationHook;
use Icinga\Authentication\Auth;
use Icinga\Web\Session;
use Icinga\Web\Url;
class Challenge2FAForm extends LoginForm
{
/**
* {@inheritdoc}
*/
public function init()
{
$this->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;
}
}

View File

@ -14,6 +14,7 @@ use Icinga\Exception\Http\HttpBadRequestException;
use Icinga\User; use Icinga\User;
use Icinga\Web\Form; use Icinga\Web\Form;
use Icinga\Web\RememberMe; use Icinga\Web\RememberMe;
use Icinga\Web\Session;
use Icinga\Web\Url; 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 // If user has 2FA enabled and the token hasn't been validated, redirect to login again, so that
// the token is challenged. // the token is challenged.
$redirect = $this->getElement('redirect'); if ($user->getTwoFactorEnabled() && ! $user->getTwoFactorSuccessful()) {
$old = $redirect->getValue(); $redirect = $this->getElement('redirect');
$new = []; $redirect->setValue(
if ($old) { Url::fromPath('authentication/login',
$new['redirect'] = $old; ['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); $this->getResponse()->setRerenderLayout(true);
return true; return true;

View File

@ -87,10 +87,10 @@ class Auth
*/ */
public function isAuthenticated() 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 !== null) {
if ($this->user->getTwoFactorEnabled() && ! $this->user->getTwoFactorSuccessful()) {
return false;
}
return true; return true;
} }
$this->authenticateFromSession(); $this->authenticateFromSession();
@ -98,7 +98,8 @@ class Auth
return false; 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; return true;
} }
@ -136,7 +137,9 @@ class Auth
} }
// don't log if 2fa hasn't been challenged yet // 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 = new AdmissionLoader();
$admissionLoader->applyRoles($user); $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));
} }
} }

View File

@ -122,6 +122,20 @@ class User
*/ */
protected $isHttpUser = false; 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 * Creates a user object given the provided information
* *
@ -646,4 +660,51 @@ class User
return $navigation; 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;
}
} }