// SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Forms\Account; use Icinga\Application\ClassLoader; use Icinga\Application\Hook\TwoFactorHook; use Icinga\Authentication\Auth; use Icinga\Authentication\TwoFactor; use Icinga\Web\Notification; use Icinga\Web\RememberMe; use Icinga\Web\Session; use ipl\Html\Attributes; use ipl\Html\FormElement\FieldsetElement; use ipl\Web\Common\CsrfCounterMeasure; use ipl\Web\Common\FormUid; use ipl\Web\Compat\CompatForm; use ipl\Web\Url; use LogicException; /** * Form for enrolling in and unenrolling from a two-factor authentication method */ class TwoFactorEnrollmentForm extends CompatForm { use CsrfCounterMeasure; use FormUid; /** @var string Form field name for the selected 2FA method */ public const METHOD = 'twofactor_method'; /** @var string The submit button to enroll into a 2FA method */ protected const SUBMIT_ENROLL = 'submit_twofactor_enroll'; /** @var string The submit button to unenroll from a 2FA method */ protected const SUBMIT_UNENROLL = 'submit_twofactor_unenroll'; /** @var bool Whether the user is already enrolled in a 2FA method */ protected bool $enrolled = false; /** * Create a new TwoFactorEnrollmentForm */ public function __construct() { $this->setAttribute('name', 'form_twofactor_enrollment'); } /** * Set whether the user is already enrolled in a 2FA method * * Must be called before the form is assembled. * * @param bool $enrolled Whether the user is enrolled * * @return $this * * @throws LogicException If called after the form has been assembled */ public function setEnrolled(bool $enrolled): static { if ($this->hasBeenAssembled) { throw new LogicException('setEnrolled() must be called before the form is assembled'); } $this->enrolled = $enrolled; return $this; } protected function assemble(): void { $this->addCsrfCounterMeasure(Session::getSession()->getId()); $this->addElement($this->createUidElement()); $methods = TwoFactorHook::all(); $this->addElement('select', static::METHOD, [ 'label' => $this->translate('2FA Method'), 'class' => 'autosubmit', 'disabled' => $this->enrolled, 'options' => array_merge( ['' => sprintf(' - %s - ', $this->translate('Please choose'))], array_combine( array_map(fn(TwoFactor $method) => $method->getName(), $methods), array_map(fn(TwoFactor $method) => $method->getDisplayName(), $methods) ) ) ]); if ($this->enrolled) { $this->addElement('submit', static::SUBMIT_UNENROLL, [ 'label' => $this->translate('Unenroll'), 'data-progress-label' => $this->translate('Unenrolling') ]); return; } $method = $this->getPopulatedValue(static::METHOD); if ($method === null) { return; } $twoFactor = TwoFactorHook::fromName($method); $configFieldset = new FieldsetElement($twoFactor->getName()); $this->addElement($configFieldset); $twoFactor->assembleEnrollmentFormElements($configFieldset); if ($configFieldset->getName() !== $twoFactor->getName()) { throw new LogicException(sprintf( '%s::assembleEnrollmentFormElements() must not rename the fieldset. The name "%s" is used' . 'as the element key in onSuccess() to retrieve the fieldset, but it was changed to "%s".', get_class($twoFactor), $twoFactor->getName(), $configFieldset->getName() )); } $configFieldset->addAttributes(Attributes::create([ 'class' => 'icinga-module module-' . ClassLoader::extractModuleName(get_class($twoFactor)) ])); $this->addElement('submit', static::SUBMIT_ENROLL, [ 'label' => $this->translate('Enroll'), 'data-progress-label' => $this->translate('Enrolling') ]); } protected function onSuccess(): void { $twoFactor = TwoFactorHook::fromName($this->getValue(static::METHOD) ?? ''); switch ($this->getPressedSubmitElement()?->getName()) { case static::SUBMIT_ENROLL: /** @var FieldsetElement $configFieldset */ $configFieldset = $this->getElement($twoFactor->getName()); if (! $twoFactor->enroll($configFieldset)) { Notification::error($this->translate('The verification failed. Please try again.')); // Don't redirect in this case, as the user might want to try again. return; } // Delete all remember-me cookies for the current user to avoid using other // sessions without a successful 2FA challenge. RememberMe::removeAllByUsername(Auth::getInstance()->getUser()->getUsername()); Notification::success(sprintf( $this->translate("Successfully enrolled in 2FA method '%s'."), $twoFactor->getDisplayName() )); $this->setRedirectUrl(Url::fromRequest()); break; case static::SUBMIT_UNENROLL: $twoFactor->unenroll(); Notification::success(sprintf( $this->translate("Successfully unenrolled from 2FA method '%s'."), $twoFactor->getDisplayName() )); $this->setRedirectUrl(Url::fromRequest()); break; } } }