mirror of
https://github.com/Icinga/icingaweb2.git
synced 2026-06-19 05:19:57 -04:00
Without unique ids, `getCSSPath()` in `loader.js` produces the same path for the submit buttons of both forms. When the login form is submitted and the container transitions to the two-factor challenge form, `renderContentToContainer()` restores focus to that path, incorrectly focusing the challenge form's submit button instead of the token input. Unique ids make the paths differ so no match is found and the autofocus class on the token input takes effect as intended.
162 lines
6.2 KiB
PHP
162 lines
6.2 KiB
PHP
<?php
|
|
|
|
// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
namespace Icinga\Forms\Authentication;
|
|
|
|
use Exception;
|
|
use Icinga\Application\Hook\AuthenticationHook;
|
|
use Icinga\Application\Hook\TwoFactorHook;
|
|
use Icinga\Application\Icinga;
|
|
use Icinga\Application\Logger;
|
|
use Icinga\Authentication\Auth;
|
|
use Icinga\Authentication\TwoFactorState;
|
|
use Icinga\Web\Form\Element\LoginRedirect;
|
|
use Icinga\Web\RememberMe;
|
|
use Icinga\Web\Session;
|
|
use Icinga\Web\Url;
|
|
use ipl\Html\Attributes;
|
|
use ipl\Html\FormDecoration\ErrorsDecorator;
|
|
use ipl\Html\FormDecoration\HtmlTagDecorator;
|
|
use ipl\Html\FormDecoration\RenderElementDecorator;
|
|
use ipl\Html\HtmlElement;
|
|
use ipl\Html\Text;
|
|
use ipl\Web\Common\CsrfCounterMeasure;
|
|
use ipl\Web\Common\FormUid;
|
|
use ipl\Web\Compat\CompatForm;
|
|
use ipl\Web\Widget\Icon;
|
|
|
|
/**
|
|
* Form for the two-factor authentication challenge
|
|
*/
|
|
class TwoFactorChallengeForm extends CompatForm
|
|
{
|
|
use CsrfCounterMeasure;
|
|
use FormUid;
|
|
|
|
/** @var string Name of the token text input in the verification form */
|
|
public const TOKEN = 'twofactor_token';
|
|
|
|
/** @var string Name of the verify submit button in the verification form */
|
|
public const SUBMIT_VERIFY = 'submit_twofactor_verify';
|
|
|
|
/** @var string Name of the cancel submit button in the verification form */
|
|
public const SUBMIT_CANCEL = 'submit_twofactor_cancel';
|
|
|
|
/**
|
|
* Create a new TwoFactorChallengeForm
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->setAttribute('name', 'form_twofactor_challenge');
|
|
// Gives this form a unique CSS path so that loader.js does not incorrectly restore focus to
|
|
// this form's submit button after the login form submits and transitions to this view.
|
|
$this->setAttribute('id', 'twofactor-challenge-form-focus-anchor');
|
|
}
|
|
|
|
protected function assemble(): void
|
|
{
|
|
$this->addCsrfCounterMeasure(Session::getSession()->getId());
|
|
$this->addElement($this->createUidElement());
|
|
|
|
$this->addElement('text', static::TOKEN, [
|
|
'autocomplete' => 'off',
|
|
'autofocus' => '',
|
|
'class' => 'content-centered',
|
|
'decorators' => [
|
|
'RenderElement' => new RenderElementDecorator(),
|
|
'Errors' => (new ErrorsDecorator())->setClass('errors'),
|
|
'ControlGroup' => (new HtmlTagDecorator())->setTag('div')->setClass('control-group')
|
|
],
|
|
'placeholder' => $this->translate('Please enter your 2FA token'),
|
|
'required' => true
|
|
]);
|
|
|
|
$this->addElement('submit', static::SUBMIT_VERIFY, [
|
|
'data-progress-label' => $this->translate('Verifying'),
|
|
'label' => $this->translate('Verify')
|
|
]);
|
|
|
|
$this->addElement('submitButton', static::SUBMIT_CANCEL, [
|
|
'class' => 'btn-back-to-login-link',
|
|
'formnovalidate' => true,
|
|
'label' => [
|
|
new Icon('arrow-left'),
|
|
HtmlElement::create('span', Attributes::create(), Text::create($this->translate('Back to login')))
|
|
]
|
|
]);
|
|
|
|
$this->addElement(new LoginRedirect('redirect', ['value' => Url::fromRequest()->getParam('redirect')]));
|
|
}
|
|
|
|
/**
|
|
* Verify the submitted two-factor token and complete login on success
|
|
*
|
|
* Retrieves the challenged user from the session and the enrolled 2FA method.
|
|
* If the enrolled method is no longer available, adds an error message and
|
|
* calls {@see onError()}. On a valid token, authenticates the user, optionally
|
|
* issues the remember-me cookie stored in the challenge state, clears the
|
|
* pending challenge from the session, triggers registered
|
|
* {@see AuthenticationHook}s, and sets the post-login redirect URL. On an
|
|
* invalid token, adds a validation error to the token field.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function onSuccess(): void
|
|
{
|
|
$twoFactorState = new TwoFactorState();
|
|
$user = $twoFactorState->getChallengedUser();
|
|
$twoFactorMethod = TwoFactorHook::loadEnrolled($user);
|
|
if ($twoFactorMethod === null) {
|
|
// This can happen when another user disables the module that provides the 2FA method,
|
|
// while the current user is still on the 2FA challenge form.
|
|
$this->addMessage($this->translate(
|
|
'Your two-factor authentication method is no longer available.'
|
|
. ' If this is unexpected, contact your administrator.'
|
|
. ' Otherwise use \'Back to login\' — you will not be prompted for a second factor.'
|
|
));
|
|
$this->onError();
|
|
|
|
return;
|
|
}
|
|
if ($twoFactorMethod->verify($this->getValue(static::TOKEN))) {
|
|
Logger::info(
|
|
'User "%s" passed two-factor verification using method "%s"',
|
|
$user->getUsername(),
|
|
$twoFactorMethod->getName()
|
|
);
|
|
Auth::getInstance()->setAuthenticated($user);
|
|
|
|
$response = Icinga::app()->getResponse();
|
|
|
|
if ($cookieData = $twoFactorState->getRememberMeCookieData()) {
|
|
try {
|
|
$rememberMe = RememberMe::fromCookieData($cookieData);
|
|
$response->setCookie($rememberMe->getCookie());
|
|
} catch (Exception $e) {
|
|
Logger::error('Failed to let user "%s" stay logged in: %s', $user->getUsername(), $e);
|
|
}
|
|
}
|
|
|
|
$twoFactorState->completeChallenge();
|
|
|
|
// Call provided AuthenticationHook(s) after successful login
|
|
AuthenticationHook::triggerLogin($user);
|
|
|
|
$response->setRerenderLayout();
|
|
/** @var LoginRedirect $redirect */
|
|
$redirect = $this->getElement('redirect');
|
|
$this->setRedirectUrl($redirect->getUrl());
|
|
|
|
return;
|
|
}
|
|
|
|
Logger::warning(
|
|
'Two-factor verification failed for user "%s" using method "%s"',
|
|
$user->getUsername(),
|
|
$twoFactorMethod->getName()
|
|
);
|
|
$this->getElement(static::TOKEN)->addMessage($this->translate('Token is invalid!'));
|
|
}
|
|
}
|