diff --git a/application/controllers/AccountController.php b/application/controllers/AccountController.php index f172cfeca..38f5abfd4 100644 --- a/application/controllers/AccountController.php +++ b/application/controllers/AccountController.php @@ -4,11 +4,14 @@ namespace Icinga\Controllers; use Icinga\Application\Config; +use Icinga\Application\Icinga; use Icinga\Authentication\User\UserBackend; use Icinga\Data\ConfigObject; use Icinga\Exception\ConfigurationError; use Icinga\Forms\Account\ChangePasswordForm; +use Icinga\Forms\Account\TotpForm; use Icinga\Forms\PreferenceForm; +use Icinga\Authentication\Totp; use Icinga\User\Preferences\PreferencesStore; use Icinga\Web\Controller; @@ -67,6 +70,26 @@ class AccountController extends Controller } } + // create a form to add and enable 2FA via TOTP + + if ( $user->can('user/two-factor-authentication') ) { + $totp = new Totp($user->getUsername()); + $totpForm = (new TotpForm()) + ->setPreferences($user->getPreferences()) + ->setTotp($totp); + if (isset($config->config_resource)) { + $totpForm->setStore(PreferencesStore::create(new ConfigObject(array( + 'resource' => $config->config_resource + )), $user)); + } + +// $db = Icinga::app()->; +// Totp::on() + $totpForm->handleRequest(); + + $this->view->totpForm = $totpForm; + } + $form = new PreferenceForm(); $form->setPreferences($user->getPreferences()); if (isset($config->config_resource)) { diff --git a/application/controllers/TryoutController.php b/application/controllers/TryoutController.php new file mode 100644 index 000000000..33245bea1 --- /dev/null +++ b/application/controllers/TryoutController.php @@ -0,0 +1,60 @@ +addContent( + HtmlElement::create( + 'h1', + null, + $this->translate('Tryout Section') + ) + ); + + +// $clock = new PsrClock(); +// $otp = TOTP::generate($clock); + +// $otp->getQrCodeUri() +// $secret = $otp->getSecret(); +// $secret = '73P442OENPZ5ZUSIWR6VGHPD4XKANATHJYFCD7SVXR2KXBOS3PJY3FHCPBM3NLAB4NMOCUP7ZC53KEQJWLUCTKQXHTIGFZOVQC77M2Y'; +// $otp = TOTP::createFromSecret($secret, $clock); + + $totp = new Totp('icingaadmi'); +// if ($totp->userHasSecret()) { + $secret = $totp->getSecret(); + $tmpSecret = $totp->generateSecret()->getTemporarySecret(); + $tmpSecret2 = $totp->generateSecret()->getTemporarySecret(); + + $this->addContent( + HtmlElement::create( + 'div', + null, + [ + HtmlElement::create('p', null, sprintf('The OTP secret is: %s', $secret)), + HtmlElement::create('p', null, sprintf('Temp OTP secret is: %s', $tmpSecret)), + HtmlElement::create('p', null, sprintf('Temp2 OTP secret is: %s', $tmpSecret2)), + HtmlElement::create('p', null, sprintf('The current OTP is: %s', $totp->getCurrentCode())) + ] + ) + ); +// } else { +// $this->addContent( +// HtmlElement::create( +// 'div', +// null, +// HtmlElement::create('p', null, 'No TOTP secret found for user icingaadmi') +// ) +// ); +// } + } +} diff --git a/application/forms/Account/TotpForm.php b/application/forms/Account/TotpForm.php new file mode 100644 index 000000000..6a5ed1da5 --- /dev/null +++ b/application/forms/Account/TotpForm.php @@ -0,0 +1,234 @@ +setName('form_totp'); + $this->setSubmitLabel($this->translate('Save Changes')); + $this->setProgressLabel($this->translate('Saving')); + } + + public function setTotp(Totp $totp): self + { + $this->totp = $totp; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData) + { + $this->addElement( + 'checkbox', + 'enabled_2fa', + [ + 'required' => false, + 'autosubmit' => true, + 'label' => $this->translate('Enable TOTP 2FA'), + 'description' => $this->translate( + 'This option allows you to enable or to disable the second factor authentication via TOTP' + ), + 'value' => '', + ] + ); + + if (isset($formData['enabled_2fa']) && $formData['enabled_2fa']) { + + $this->addElement( + 'text', + 'totp_secret', + [ + 'label' => $this->translate('TOTP Secret:'), + 'value' => $this->totp->getSecret() ?? $this->translate('No Secret set'), + 'description' => $this->translate( + 'If you generate a new TOTP secret, you will need to reconfigure your TOTP application with the new secret. ' . + 'If you reset the TOTP secret, you will lose access to your TOTP application and will need to set it up again.' + ), + 'disabled' => true, +// 'decorators' => ['ViewHelper'] + ] + ); + + if ($this->totp->getSecret() !== null) { + $this->addElement( + 'submit', + 'btn_renew_totp', + array( + 'ignore' => true, + 'label' => $this->translate('Renew TOTP Secret'), + 'decorators' => array('ViewHelper'), + ) + ); + + $this->addElement( + 'submit', + 'btn_delete_totp', + array( + 'ignore' => true, + 'label' => $this->translate('Delete TOTP Secret'), + 'decorators' => array('ViewHelper') + ) + ); + } else { + $this->addElement( + 'submit', + 'btn_generate_totp', + array( + 'ignore' => true, + 'label' => $this->translate('Generate TOTP Secret'), + 'decorators' => array('ViewHelper') + ) + ); + } + } + + $this->addElement( + 'submit', + 'btn_submit', + array( + 'ignore' => true, + 'label' => $this->translate('Save to the Preferences'), + 'decorators' => array('ViewHelper'), + 'class' => 'btn-primary' + ) + ); + + $this->addDisplayGroup( + array('btn_submit', 'btn_delete_totp', 'btn_renew_totp', 'btn_generate_totp'), + 'submit_buttons', + array( + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls')) + ) + ) + ); + } + + /** + * {@inheritdoc} + */ + public function onSuccess() + { + + + try { + if ($this->getElement('btn_submit') && $this->getElement('btn_submit')->isChecked()) { + $this->preferences = new Preferences($this->store ? $this->store->load() : array()); + $webPreferences = $this->preferences->get('icingaweb'); + foreach ($this->getValues() as $key => $value) { + if ($value === '' + || $value === null + || $value === 'autodetect' + ) { + if (isset($webPreferences[$key])) { + unset($webPreferences[$key]); + } + } else { + $webPreferences[$key] = $value; + } + } + $this->preferences->icingaweb = $webPreferences; + Session::getSession()->user->setPreferences($this->preferences); + $this->save(); + Notification::success($this->translate('Submitted btn_submit')); + + return true; + } elseif ($this->getElement('btn_generate_totp') && $this->getElement('btn_generate_totp')->isChecked()) { + Notification::success($this->translate('Submitted btn_generate_totp')); + + return true; + } elseif ($this->getElement('btn_renew_totp') && $this->getElement('btn_renew_totp')->isChecked()) { + Notification::success($this->translate('Submitted btn_renew_totp')); + + return true; + } elseif ($this->getElement('btn_delete_totp') && $this->getElement('btn_delete_totp')->isChecked()) { + Notification::info($this->translate('Submitted btn_delete_totp')); + + return false; + } + } catch (Exception $e) { + Logger::error($e); + Notification::error($e->getMessage()); + } + + return false; + } + + /** + * Populate preferences + * + * @see Form::onRequest() + */ + public function onRequest() + { + $auth = Auth::getInstance(); + $values = $auth->getUser()->getPreferences()->get('icingaweb'); + + if (!isset($values['enabled_2fa'])) { + $values['enabled_2fa'] = '0'; + } + + $this->populate($values); + } + + public function isSubmitted() + { + if ( + ($this->getElement('btn_generate_totp') && $this->getElement('btn_generate_totp')->isChecked()) + || ($this->getElement('btn_renew_totp') && $this->getElement('btn_renew_totp')->isChecked()) + || ($this->getElement('btn_delete_totp') && $this->getElement('btn_delete_totp')->isChecked()) + || ($this->getElement('btn_submit') && $this->getElement('btn_submit')->isChecked()) + ) { + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ +// public function isValid($formData) +// { +//// $valid = parent::isValid($formData); +//// if (! $valid) { +//// return false; +//// } +//// +//// $oldPasswordEl = $this->getElement('old_password'); +//// +//// if (! $this->backend->authenticate($this->Auth()->getUser(), $oldPasswordEl->getValue())) { +//// $oldPasswordEl->addError($this->translate('Old password is invalid')); +//// $this->markAsError(); +//// return false; +//// } +//// +//// return true; +// } +} diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index 58387f7aa..4ab126373 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -573,6 +573,9 @@ class RoleForm extends RepositoryForm 'user/password-change' => [ 'description' => t('Allow password changes in the account preferences') ], + 'user/two-factor-authentication'=> [ + 'description' => t('Allow 2FA configuration in the account preferences') + ], 'user/application/stacktraces' => [ 'description' => t('Allow to adjust in the preferences whether to show stacktraces') ], diff --git a/application/views/scripts/account/index.phtml b/application/views/scripts/account/index.phtml index efc2bcbf6..c48b721fe 100644 --- a/application/views/scripts/account/index.phtml +++ b/application/views/scripts/account/index.phtml @@ -5,6 +5,10 @@