From 4479297682f111f0f9bb847cc0c35a5ab19093ab Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 18 Nov 2020 16:02:35 +0100 Subject: [PATCH] Support multi-factor authentication within ACP --- .../templates/multifactorAuthentication.tpl | 2 +- syncTemplates.json | 3 + .../__multifactorBackupCodeField.tpl | 17 ++ .../templates/__multifactorEmailCodeField.tpl | 17 ++ .../templates/__multifactorTotpCodeField.tpl | 17 ++ .../templates/multifactorAuthentication.tpl | 44 +++++ .../files/lib/acp/form/LoginForm.class.php | 2 +- ...ltifactorAuthenticationAbortForm.class.php | 86 +++++++++ .../MultifactorAuthenticationForm.class.php | 180 ++++++++++++++++++ .../install/files/lib/system/WCFACP.class.php | 2 +- 10 files changed, 367 insertions(+), 3 deletions(-) create mode 100644 wcfsetup/install/files/acp/templates/__multifactorBackupCodeField.tpl create mode 100644 wcfsetup/install/files/acp/templates/__multifactorEmailCodeField.tpl create mode 100644 wcfsetup/install/files/acp/templates/__multifactorTotpCodeField.tpl create mode 100644 wcfsetup/install/files/acp/templates/multifactorAuthentication.tpl create mode 100644 wcfsetup/install/files/lib/acp/form/MultifactorAuthenticationAbortForm.class.php create mode 100644 wcfsetup/install/files/lib/acp/form/MultifactorAuthenticationForm.class.php diff --git a/com.woltlab.wcf/templates/multifactorAuthentication.tpl b/com.woltlab.wcf/templates/multifactorAuthentication.tpl index 35c057353c..611f8ee856 100644 --- a/com.woltlab.wcf/templates/multifactorAuthentication.tpl +++ b/com.woltlab.wcf/templates/multifactorAuthentication.tpl @@ -2,7 +2,7 @@ {capture assign='contentTitle'}{lang}wcf.user.security.multifactor.authentication{/lang}{/capture} {capture assign='sidebarLeft'} -
+

{lang}wcf.user.security.multifactor.methods{/lang}

diff --git a/syncTemplates.json b/syncTemplates.json index f2232326c4..4d0d570e82 100644 --- a/syncTemplates.json +++ b/syncTemplates.json @@ -26,6 +26,9 @@ "__labelFormField", "__mediaSetCategoryDialog", "__messageQuoteManager", + "__multifactorBackupCodeField", + "__multifactorEmailCodeField", + "__multifactorTotpCodeField", "__multilineTextFormField", "__multiPageCondition", "__multipleSelectionFormField", diff --git a/wcfsetup/install/files/acp/templates/__multifactorBackupCodeField.tpl b/wcfsetup/install/files/acp/templates/__multifactorBackupCodeField.tpl new file mode 100644 index 0000000000..d76256140e --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__multifactorBackupCodeField.tpl @@ -0,0 +1,17 @@ +getChunks() && $field->getChunkLength()} size="{$field->getChunks() - 1 + $field->getChunks() * $field->getChunkLength()}"{/if}{* + *}{if $field->isAutofocused()} autofocus{/if}{* + *}{if $field->isRequired()} required{/if}{* + *}{if $field->isImmutable()} disabled{/if}{* + *}{if $field->getMinimumLength() !== null} minlength="{$field->getMinimumLength()}"{/if}{* + *}{if $field->getMaximumLength() !== null} maxlength="{$field->getMaximumLength()}"{/if}{* + *}{if $field->getPlaceholder() !== null} placeholder="{$field->getPlaceholder()}"{/if}{* + *}{if $field->getDocument()->isAjax()} data-dialog-submit-on-enter="true"{/if}{* +*}> diff --git a/wcfsetup/install/files/acp/templates/__multifactorEmailCodeField.tpl b/wcfsetup/install/files/acp/templates/__multifactorEmailCodeField.tpl new file mode 100644 index 0000000000..331481b5b7 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__multifactorEmailCodeField.tpl @@ -0,0 +1,17 @@ +getMaximumLength() !== null}size="{$field->getMaximumLength()}" {/if}{* + *}pattern="[0-9]*" {* + *}inputmode="numeric"{* + *}{if $field->isAutofocused()} autofocus{/if}{* + *}{if $field->isRequired()} required{/if}{* + *}{if $field->isImmutable()} disabled{/if}{* + *}{if $field->getMinimumLength() !== null} minlength="{$field->getMinimumLength()}"{/if}{* + *}{if $field->getMaximumLength() !== null} maxlength="{$field->getMaximumLength()}"{/if}{* + *}{if $field->getPlaceholder() !== null} placeholder="{$field->getPlaceholder()}"{/if}{* + *}{if $field->getDocument()->isAjax()} data-dialog-submit-on-enter="true"{/if}{* +*}> diff --git a/wcfsetup/install/files/acp/templates/__multifactorTotpCodeField.tpl b/wcfsetup/install/files/acp/templates/__multifactorTotpCodeField.tpl new file mode 100644 index 0000000000..9b46101f55 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__multifactorTotpCodeField.tpl @@ -0,0 +1,17 @@ +getMaximumLength() !== null}size="{$field->getMaximumLength()}" {/if}{* + *}pattern="[0-9]*" {* + *}inputmode="numeric"{* + *}{if $field->isAutofocused()} autofocus{/if}{* + *}{if $field->isRequired()} required{/if}{* + *}{if $field->isImmutable()} disabled{/if}{* + *}{if $field->getMinimumLength() !== null} minlength="{$field->getMinimumLength()}"{/if}{* + *}{if $field->getMaximumLength() !== null} maxlength="{$field->getMaximumLength()}"{/if}{* + *}{if $field->getPlaceholder() !== null} placeholder="{$field->getPlaceholder()}"{/if}{* + *}{if $field->getDocument()->isAjax()} data-dialog-submit-on-enter="true"{/if}{* +*}> diff --git a/wcfsetup/install/files/acp/templates/multifactorAuthentication.tpl b/wcfsetup/install/files/acp/templates/multifactorAuthentication.tpl new file mode 100644 index 0000000000..ebd4446c78 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/multifactorAuthentication.tpl @@ -0,0 +1,44 @@ +{capture assign='pageTitle'}{lang}wcf.user.security.multifactor.authentication{/lang}{/capture} +{capture assign='contentTitle'}{lang}wcf.user.security.multifactor.authentication{/lang}{/capture} + +{include file='header' __isLogin=true} + +
+

{lang}wcf.user.security.multifactor.methods{/lang}

+ +
+ +
+
+ +
+ {@$userProfile->getAvatar()->getImageTag(48)} + +
+
+

+ {lang}wcf.user.security.multifactor.authentication.user.headline{/lang} +

+
+
+ {lang}wcf.user.security.multifactor.authentication.user.content{/lang} +
+ +
+ + {csrfToken} +
+
+
+ +{@$form->getHtml()} + +{include file='footer'} diff --git a/wcfsetup/install/files/lib/acp/form/LoginForm.class.php b/wcfsetup/install/files/lib/acp/form/LoginForm.class.php index 4d5b28566c..404dbc7bb1 100755 --- a/wcfsetup/install/files/lib/acp/form/LoginForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/LoginForm.class.php @@ -213,7 +213,7 @@ class LoginForm extends AbstractCaptchaForm { */ protected function performRedirect(bool $needsMultifactor = false) { if ($needsMultifactor) { - $this->url = LinkHandler::getInstance()->getControllerLink(MultifactorAuthenticationForm::class, [ + $this->url = LinkHandler::getInstance()->getLink('MultifactorAuthentication', [ 'url' => $this->url, ]); } diff --git a/wcfsetup/install/files/lib/acp/form/MultifactorAuthenticationAbortForm.class.php b/wcfsetup/install/files/lib/acp/form/MultifactorAuthenticationAbortForm.class.php new file mode 100644 index 0000000000..8ca808df63 --- /dev/null +++ b/wcfsetup/install/files/lib/acp/form/MultifactorAuthenticationAbortForm.class.php @@ -0,0 +1,86 @@ + + * @package WoltLabSuite\Core\Acp\Form + * @since 5.4 + */ +class MultifactorAuthenticationAbortForm extends AbstractForm { + const AVAILABLE_DURING_OFFLINE_MODE = true; + + /** + * @inheritDoc + */ + public $useTemplate = false; + + /** + * @inheritDoc + */ + public function readParameters() { + parent::readParameters(); + + if (WCF::getUser()->userID) { + throw new PermissionDeniedException(); + } + + $user = WCF::getSession()->getPendingUserChange(); + if (!$user) { + $this->performRedirect(); + } + } + + /** + * @inheritDoc + */ + public function save() { + parent::save(); + + WCF::getSession()->clearPendingUserChange(); + + $this->saved(); + } + + /** + * @inheritDoc + */ + public function saved() { + parent::saved(); + + $this->performRedirect(); + } + + /** + * Returns to the landing page otherwise. + */ + protected function performRedirect() { + HeaderUtil::redirect( + LinkHandler::getInstance()->getControllerLink(LoginForm::class) + ); + exit; + } + + /** + * @inheritDoc + */ + public function show() { + parent::show(); + + // It is not expected to reach this place, because the form should + // never be accessed via a direct link. + // If we reach it nonetheless we simply redirect back to the authentication + // form which contains the proper button to perform the submission. + HeaderUtil::redirect(LinkHandler::getInstance()->getControllerLink(MultifactorAuthenticationForm::class)); + exit; + } +} diff --git a/wcfsetup/install/files/lib/acp/form/MultifactorAuthenticationForm.class.php b/wcfsetup/install/files/lib/acp/form/MultifactorAuthenticationForm.class.php new file mode 100644 index 0000000000..ffa6e55cc3 --- /dev/null +++ b/wcfsetup/install/files/lib/acp/form/MultifactorAuthenticationForm.class.php @@ -0,0 +1,180 @@ + + * @package WoltLabSuite\Core\Acp\Form + * @since 5.4 + */ +class MultifactorAuthenticationForm extends AbstractFormBuilderForm { + const AVAILABLE_DURING_OFFLINE_MODE = true; + + /** + * @inheritDoc + */ + public $formAction = 'authenticate'; + + /** + * @var User + */ + private $user; + + /** + * @var Setup[] + */ + private $setups; + + /** + * @var ObjectType + */ + private $method; + + /** + * @var IMultifactorMethod + */ + private $processor; + + /** + * @var Setup + */ + private $setup; + + /** + * @var string + */ + public $redirectUrl; + + /** + * @inheritDoc + */ + public function readParameters() { + parent::readParameters(); + + if (!empty($_GET['url']) && ApplicationHandler::getInstance()->isInternalURL($_GET['url'])) { + $this->redirectUrl = $_GET['url']; + } + + if (WCF::getUser()->userID) { + $this->performRedirect(); + } + + $this->user = WCF::getSession()->getPendingUserChange(); + if (!$this->user) { + throw new PermissionDeniedException(); + } + + $this->setups = Setup::getAllForUser($this->user); + + if (empty($this->setups)) { + throw new \LogicException('Unreachable'); + } + + \uasort($this->setups, function (Setup $a, Setup $b) { + return $b->getObjectType()->priority <=> $a->getObjectType()->priority; + }); + + $setupId = \array_keys($this->setups)[0]; + if (isset($_GET['id'])) { + $setupId = intval($_GET['id']); + } + + if (!isset($this->setups[$setupId])) { + throw new IllegalLinkException(); + } + + $this->setup = $this->setups[$setupId]; + $this->method = $this->setup->getObjectType(); + \assert($this->method->getDefinition()->definitionName === 'com.woltlab.wcf.multifactor'); + + $this->processor = $this->method->getProcessor(); + } + + /** + * @inheritDoc + */ + protected function createForm() { + parent::createForm(); + + $this->processor->createAuthenticationForm($this->form, $this->setup); + } + + public function save() { + AbstractForm::save(); + + WCF::getDB()->beginTransaction(); + + $setup = $this->setup->lock(); + + $this->returnData = $this->processor->processAuthenticationForm($this->form, $setup); + + WCF::getDB()->commitTransaction(); + + WCF::getSession()->applyPendingUserChange($this->user); + + $this->saved(); + } + + /** + * @inheritDoc + */ + public function saved() { + AbstractForm::saved(); + + $this->performRedirect(); + } + + /** + * Returns to the redirectUrl if given and to the landing page otherwise. + */ + protected function performRedirect() { + if ($this->redirectUrl) { + HeaderUtil::redirect($this->redirectUrl); + } + else { + HeaderUtil::redirect(LinkHandler::getInstance()->getLink()); + } + exit; + } + + /** + * @inheritDoc + */ + protected function setFormAction() { + $this->form->action(LinkHandler::getInstance()->getControllerLink(static::class, [ + 'object' => $this->setup, + 'url' => $this->redirectUrl, + ])); + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign([ + 'setups' => $this->setups, + 'user' => $this->user, + 'userProfile' => UserProfileRuntimeCache::getInstance()->getObject($this->user->userID), + 'setup' => $this->setup, + 'redirectUrl' => $this->redirectUrl, + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/WCFACP.class.php b/wcfsetup/install/files/lib/system/WCFACP.class.php index b29eeb1898..447b4b4b25 100644 --- a/wcfsetup/install/files/lib/system/WCFACP.class.php +++ b/wcfsetup/install/files/lib/system/WCFACP.class.php @@ -139,7 +139,7 @@ class WCFACP extends WCF { exit; } } - else if (empty($pathInfo) || !preg_match('~^/?(login|logout)/~i', $pathInfo)) { + else if (empty($pathInfo) || !preg_match('~^/?(login|logout|multifactor-authentication|multifactor-authentication-abort)/~i', $pathInfo)) { if (WCF::getUser()->userID == 0) { // work-around for AJAX-requests within ACP if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { -- 2.20.1