From: Tim Düsterhus Date: Fri, 20 Nov 2020 14:25:47 +0000 (+0100) Subject: Support disabling the multi-factor authentication X-Git-Tag: 5.4.0_Alpha_1~555^2~39^2 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=83c7f91eb09281c2c24f82465954f888c01f73d0;p=GitHub%2FWoltLab%2FWCF.git Support disabling the multi-factor authentication --- diff --git a/com.woltlab.wcf/page.xml b/com.woltlab.wcf/page.xml index e6b3932403..7f7d66e625 100644 --- a/com.woltlab.wcf/page.xml +++ b/com.woltlab.wcf/page.xml @@ -831,8 +831,8 @@ E-Mail: [E-Mail-Adresse der verantwortlichen Stelle]


Verantwort system wcf\form\MultifactorManageForm - Mehrfaktor-Authentifizierung einrichten Manage Multi-Factor Authentication + Mehrfaktor-Authentifizierung einrichten 1 com.woltlab.wcf.AccountSecurity 1 @@ -849,6 +849,23 @@ E-Mail: [E-Mail-Adresse der verantwortlichen Stelle]


Verantwort 1 1 + + system + wcf\form\MultifactorDisableForm + Disable Multi-Factor Authentication + Mehrfaktor-Authentifizierung deaktivieren + 1 + com.woltlab.wcf.AccountSecurity + 1 + 1 + 1 + + Disable Multi-Factor Authentication + + + Mehrfaktor-Authentifizierung deaktivieren + + diff --git a/com.woltlab.wcf/templates/__multifactorDisableExplanation.tpl b/com.woltlab.wcf/templates/__multifactorDisableExplanation.tpl new file mode 100644 index 0000000000..a6c516e47e --- /dev/null +++ b/com.woltlab.wcf/templates/__multifactorDisableExplanation.tpl @@ -0,0 +1 @@ +{lang}wcf.user.security.multifactor.disable.explanation{/lang} diff --git a/com.woltlab.wcf/templates/accountSecurity.tpl b/com.woltlab.wcf/templates/accountSecurity.tpl index 8e2c030cf1..4f079fb8c8 100644 --- a/com.woltlab.wcf/templates/accountSecurity.tpl +++ b/com.woltlab.wcf/templates/accountSecurity.tpl @@ -36,9 +36,21 @@

- - {lang}wcf.user.security.multifactor.{if $enabledMultifactorMethods[$method->objectTypeID]|isset}manage{else}setup{/if}{/lang} - + {if $enabledMultifactorMethods[$method->objectTypeID]|isset} + {if $method->objectType !== 'com.woltlab.wcf.multifactor.backup'} + + {lang}wcf.user.security.multifactor.disable{/lang} + + {/if} + + + {lang}wcf.user.security.multifactor.manage{/lang} + + {else} + + {lang}wcf.user.security.multifactor.setup{/lang} + + {/if}
diff --git a/com.woltlab.wcf/templates/multifactorDisable.tpl b/com.woltlab.wcf/templates/multifactorDisable.tpl new file mode 100644 index 0000000000..9b9926bfc8 --- /dev/null +++ b/com.woltlab.wcf/templates/multifactorDisable.tpl @@ -0,0 +1,7 @@ +{include file='userMenuSidebar'} + +{include file='header' __disableAds=true __sidebarLeftHasMenu=true} + +{@$form->getHtml()} + +{include file='footer' __disableAds=true} diff --git a/com.woltlab.wcf/templates/multifactorManage.tpl b/com.woltlab.wcf/templates/multifactorManage.tpl index b1c9d6aab7..ada92d3122 100644 --- a/com.woltlab.wcf/templates/multifactorManage.tpl +++ b/com.woltlab.wcf/templates/multifactorManage.tpl @@ -5,7 +5,6 @@ {include file='header' __disableAds=true __sidebarLeftHasMenu=true} - {if $backupForm} {if $form->showsSuccessMessage()}

diff --git a/wcfsetup/install/files/lib/form/MultifactorDisableForm.class.php b/wcfsetup/install/files/lib/form/MultifactorDisableForm.class.php new file mode 100644 index 0000000000..8f4a6c4f25 --- /dev/null +++ b/wcfsetup/install/files/lib/form/MultifactorDisableForm.class.php @@ -0,0 +1,231 @@ + + * @package WoltLabSuite\Core\Form + * @since 5.4 + */ +class MultifactorDisableForm extends AbstractFormBuilderForm { + /** + * @inheritDoc + */ + public $loginRequired = true; + + /** + * @var ObjectType + */ + private $method; + + /** + * @var Setup + */ + private $setup; + + /** + * @var Setup[] + */ + private $setups; + + /** + * @inheritDoc + */ + public function readParameters() { + parent::readParameters(); + + if (!isset($_GET['id'])) { + throw new IllegalLinkException(); + } + + $this->setups = Setup::getAllForUser(WCF::getUser()); + + if (empty($this->setups)) { + throw new IllegalLinkException(); + } + + if (!isset($this->setups[$_GET['id']])) { + throw new IllegalLinkException(); + } + + $this->setup = $this->setups[$_GET['id']]; + $this->method = $this->setup->getObjectType(); + \assert($this->method->getDefinition()->definitionName === 'com.woltlab.wcf.multifactor'); + + // Backup codes may not be disabled. + if ($this->method->objectType === 'com.woltlab.wcf.multifactor.backup') { + throw new PermissionDeniedException(); + } + } + + /** + * @inheritDoc + */ + protected function createForm() { + parent::createForm(); + + $this->form->appendChildren([ + TemplateFormNode::create('explanation') + ->templateName('__multifactorDisableExplanation') + ->variables([ + 'remaining' => $this->setupsWithoutDisableRequest( + $this->setupsWithoutBackupCodes($this->setups) + ), + 'setup' => $this->setup, + ]), + BooleanFormField::create('confirm') + ->label('wcf.user.security.multifactor.disable.confirm', [ + 'remaining' => $this->setupsWithoutDisableRequest( + $this->setupsWithoutBackupCodes($this->setups) + ), + 'setup' => $this->setup, + ]) + ->addValidator(new FormFieldValidator('confirm', function(BooleanFormField $formField) { + if (!$formField->getValue()) { + $formField->addValidationError( + new FormFieldValidationError( + 'required', + 'wcf.user.security.multifactor.disable.confirm.required' + ) + ); + } + })), + ]); + } + + /** + * @inheritDoc + */ + public function save() { + AbstractForm::save(); + + WCF::getDB()->beginTransaction(); + + $this->form->successMessage('wcf.user.security.multifactor.disable.success', [ + 'setup' => $this->setup, + ]); + $this->setup->delete(); + + $setups = Setup::getAllForUser(WCF::getUser()); + $remaining = $this->setupsWithoutBackupCodes($setups); + + if (empty($remaining)) { + foreach ($setups as $setup) { + $setup->delete(); + } + $this->disableMultifactorAuth(); + $this->form->successMessage('wcf.user.security.multifactor.disable.success.full'); + } + + WCF::getDB()->commitTransaction(); + + $this->saved(); + } + + /** + * @inheritDoc + */ + public function saved() { + AbstractForm::saved(); + + HeaderUtil::delayedRedirect( + LinkHandler::getInstance() + ->getControllerLink(AccountSecurityPage::class), + $this->form->getSuccessMessage() + ); + exit; + } + + /** + * Returns the active setups without the backup codes. + * + * @param Setup[] $setups + * @return Setup[] + */ + protected function setupsWithoutBackupCodes(array $setups): array { + return array_filter($setups, function (Setup $setup) { + return $setup->getObjectType()->objectType !== 'com.woltlab.wcf.multifactor.backup'; + }); + } + + /** + * Returns the active setups without the setup that is going to be disabled. + * + * @param Setup[] $setups + * @return Setup[] + */ + protected function setupsWithoutDisableRequest(array $setups): array { + return array_filter($setups, function (Setup $setup) { + return $setup->getId() !== $this->setup->getId(); + }); + } + + /** + * Disables multifactor authentication for the user. + */ + protected function disableMultifactorAuth(): void { + // This method intentionally does not use UserAction to prevent + // events from firing. + // + // This method is being run from within a transaction to ensure + // a consistent database state in case any part of the MFA setup + // fails. Event listeners could run complex logic, including + // queries that modify the database state, possibly leading to + // a very large transaction and much more surface area for + // unexpected failures. + // + // Use the saved@MultifactorDisableForm event if you need to run + // logic in response to a user disabling MFA. + $editor = new UserEditor(WCF::getUser()); + $editor->update([ + 'multifactorActive' => 0, + ]); + } + + /** + * @inheritDoc + */ + protected function setFormAction() { + $this->form->action(LinkHandler::getInstance()->getControllerLink(static::class, [ + 'object' => $this->setup, + ])); + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign([ + 'method' => $this->method, + 'setups' => $this->setups, + ]); + } + + /** + * @inheritDoc + */ + public function show() { + UserMenu::getInstance()->setActiveMenuItem('wcf.user.menu.profile.security'); + + parent::show(); + } +} diff --git a/wcfsetup/install/files/lib/form/MultifactorManageForm.class.php b/wcfsetup/install/files/lib/form/MultifactorManageForm.class.php index a9592aa91c..1dad21c363 100644 --- a/wcfsetup/install/files/lib/form/MultifactorManageForm.class.php +++ b/wcfsetup/install/files/lib/form/MultifactorManageForm.class.php @@ -98,6 +98,9 @@ class MultifactorManageForm extends AbstractFormBuilderForm { $this->processor->createManagementForm($this->form, $this->setup, $this->returnData); } + /** + * @inheritDoc + */ public function save() { AbstractForm::save(); @@ -221,7 +224,7 @@ class MultifactorManageForm extends AbstractFormBuilderForm { */ protected function setFormAction() { $this->form->action(LinkHandler::getInstance()->getControllerLink(static::class, [ - 'id' => $this->method->objectTypeID, + 'object' => $this->method, ])); } diff --git a/wcfsetup/install/files/lib/system/user/multifactor/Setup.class.php b/wcfsetup/install/files/lib/system/user/multifactor/Setup.class.php index 7548bfbe47..1fd920740c 100644 --- a/wcfsetup/install/files/lib/system/user/multifactor/Setup.class.php +++ b/wcfsetup/install/files/lib/system/user/multifactor/Setup.class.php @@ -22,6 +22,8 @@ final class Setup implements IIDObject { */ private $row; + private $isDeleted = false; + private function __construct(array $row) { $this->row = $row; } @@ -30,6 +32,10 @@ final class Setup implements IIDObject { * Returns the setup ID. */ public function getId(): int { + if ($this->isDeleted) { + throw new \BadMethodCallException('The Setup is deleted.'); + } + return $this->row['setupID']; } @@ -37,6 +43,10 @@ final class Setup implements IIDObject { * @see Setup::getId() */ public function getObjectID(): int { + if ($this->isDeleted) { + throw new \BadMethodCallException('The Setup is deleted.'); + } + return $this->getId(); } @@ -44,6 +54,10 @@ final class Setup implements IIDObject { * Returns the object type. */ public function getObjectType(): ObjectType { + if ($this->isDeleted) { + throw new \BadMethodCallException('The Setup is deleted.'); + } + return ObjectTypeCache::getInstance()->getObjectType($this->row['objectTypeID']); } @@ -51,6 +65,10 @@ final class Setup implements IIDObject { * Returns the user. */ public function getUser(): User { + if ($this->isDeleted) { + throw new \BadMethodCallException('The Setup is deleted.'); + } + return UserRuntimeCache::getInstance()->getObject($this->row['userID']); } @@ -58,6 +76,10 @@ final class Setup implements IIDObject { * Locks the database record for this setup, preventing concurrent changes, and returns itself. */ public function lock(): self { + if ($this->isDeleted) { + throw new \BadMethodCallException('The Setup is deleted.'); + } + $sql = "SELECT setupId FROM wcf".WCF_N."_user_multifactor WHERE setupId = ? @@ -73,6 +95,19 @@ final class Setup implements IIDObject { return $this; } + /** + * Deletes the setup. + */ + public function delete(): void { + $sql = "DELETE FROM wcf".WCF_N."_user_multifactor + WHERE setupId = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + $this->getId(), + ]); + $this->isDeleted = true; + } + /** * Returns an existing setup for the given objectType and user or null if none was found. */ diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index fe46550090..994acdc89d 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -4904,6 +4904,22 @@ Die E-Mail-Adresse des neuen Benutzers lautet: {@$user->email} {if LANGUAGE_USE_INFORMAL_VARIANT}Du erhältst bei jedem Login einen Einmalcode an deine E-Mail-Adresse.{else}Sie erhalten bei jedem Login einen Einmalcode an Ihre E-Mail-Adresse.{/if}

]]> Account-Verwaltung ändern.]]> + + Mit dem Absenden dieses Formulars {if LANGUAGE_USE_INFORMAL_VARIANT}deaktivierst du{else}deaktivieren Sie{/if} das Verfahren {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang} zur Nutzung mit der Mehrfaktor-Authentifizierung. {if LANGUAGE_USE_INFORMAL_VARIANT}Du wirst{else}Sie werden{/if} dieses Verfahren anschließend nicht mehr nutzen können, um {if LANGUAGE_USE_INFORMAL_VARIANT}dich{else}sich{/if} zu authentifizieren.

+{if !$remaining|empty} +

Nach Deaktivierung von {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang} {plural value=$remaining|count 1='steht folgendes' other='stehen folgende'} Verfahren weiterhin zur Mehrfaktor-Authentifizierung zur Verfügung.

+ +{else} +

Die Deaktivierung des Verfahrens wird die Mehrfaktor-Authentifizierung für {if LANGUAGE_USE_INFORMAL_VARIANT}dein{else}Ihr{/if} Benutzerkonto vollständig deaktivieren, da es das einzige aktive Verfahren ist.

+{/if}]]>
+ {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang} deaktivieren{/if}]]> + + {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang} wurde erfolgreich deaktiviert.]]> + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 208a50ceb3..f440faba8f 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -4901,6 +4901,22 @@ Open the link below to access the user profile: You will receive a one time code via email after logging in.

]]>
Account Management Form.]]> + + By submitting this form the {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang} method for multi-factor authentication will be disabled. You will no longer be able to use this method to authenticate yourself.

+{if !$remaining|empty} +

After disabling {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang} the following methods will still be available for multi-factor authentication.

+ +{else} +

Disabling {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang} will fully disable the use of multi-factor authentication for your account as it is the only active method.

+{/if}]]>
+ {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang}{/if}]]> + + {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang} method has successfully been disabled.]]> +