Support multi-factor authentication within ACP
authorTim Düsterhus <duesterhus@woltlab.com>
Wed, 18 Nov 2020 15:02:35 +0000 (16:02 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Fri, 20 Nov 2020 09:20:50 +0000 (10:20 +0100)
com.woltlab.wcf/templates/multifactorAuthentication.tpl
syncTemplates.json
wcfsetup/install/files/acp/templates/__multifactorBackupCodeField.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__multifactorEmailCodeField.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__multifactorTotpCodeField.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/multifactorAuthentication.tpl [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/LoginForm.class.php
wcfsetup/install/files/lib/acp/form/MultifactorAuthenticationAbortForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/MultifactorAuthenticationForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/WCFACP.class.php

index 35c057353ca8a5bea640ac91ded0607c1ad9eb4a..611f8ee856be677a099eb3946788942ce12b9fcb 100644 (file)
@@ -2,7 +2,7 @@
 {capture assign='contentTitle'}{lang}wcf.user.security.multifactor.authentication{/lang}{/capture}
 
 {capture assign='sidebarLeft'}
-<section class="box">
+       <section class="box">
                <h2 class="boxTitle">{lang}wcf.user.security.multifactor.methods{/lang}</h2>
                
                <div class="boxContent">
index f2232326c43b23ebd62a78fdc676257c4116fdf7..4d0d570e82740b34b0d3dcb0f0a0bed868c9f20c 100644 (file)
@@ -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 (file)
index 0000000..d762561
--- /dev/null
@@ -0,0 +1,17 @@
+<input type="text" {*
+       *}id="{@$field->getPrefixedId()}" {*
+       *}name="{@$field->getPrefixedId()}" {*
+       *}value="{if !$field->isI18n() || !$field->hasI18nValues() || $availableLanguages|count === 1}{$field->getValue()}{/if}" {*
+       *}class="multifactorBackupCode" {*
+       *}autocomplete="off" {*
+       *}pattern="[0-9\s]*" {*
+       *}inputmode="numeric"{*
+       *}{if $field->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 (file)
index 0000000..331481b
--- /dev/null
@@ -0,0 +1,17 @@
+<input type="text" {*
+       *}id="{@$field->getPrefixedId()}" {*
+       *}name="{@$field->getPrefixedId()}" {*
+       *}value="{if !$field->isI18n() || !$field->hasI18nValues() || $availableLanguages|count === 1}{$field->getValue()}{/if}" {*
+       *}class="multifactorEmailCode" {*
+       *}autocomplete="off" {*
+       *}{if $field->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 (file)
index 0000000..9b46101
--- /dev/null
@@ -0,0 +1,17 @@
+<input type="text" {*
+       *}id="{@$field->getPrefixedId()}" {*
+       *}name="{@$field->getPrefixedId()}" {*
+       *}value="{if !$field->isI18n() || !$field->hasI18nValues() || $availableLanguages|count === 1}{$field->getValue()}{/if}" {*
+       *}class="multifactorTotpCode" {*
+       *}autocomplete="off" {*
+       *}{if $field->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 (file)
index 0000000..ebd4446
--- /dev/null
@@ -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}
+
+<section class="box">
+       <h2 class="boxTitle">{lang}wcf.user.security.multifactor.methods{/lang}</h2>
+       
+       <div class="boxContent">
+               <nav>
+                       <ol class="boxMenu">
+                               {foreach from=$setups item='_setup'}
+                                       <li{if $setup->getId() == $_setup->getId()} class="active"{/if}>
+                                               <a class="boxMenuLink" href="{link controller='MultifactorAuthentication' object=$_setup url=$redirectUrl}{/link}"><span class="boxMenuLinkTitle">{lang}wcf.user.security.multifactor.{$_setup->getObjectType()->objectType}{/lang}</span></a>
+                                       </li>
+                               {/foreach}
+                       </ol>
+               </nav>
+       </div>
+</section>
+
+<div class="section box48">
+       {@$userProfile->getAvatar()->getImageTag(48)}
+       
+       <div>
+               <div class="containerHeadline">
+                       <h3>
+                               {lang}wcf.user.security.multifactor.authentication.user.headline{/lang}
+                       </h3>
+               </div>
+               <div class="containerContent">
+                       {lang}wcf.user.security.multifactor.authentication.user.content{/lang}
+               </div>
+               
+               <form action="{link controller='MultifactorAuthenticationAbort'}{/link}" method="post">
+                       <button type="submit">{lang}wcf.user.security.multifactor.authentication.logout{/lang}</button>
+                       {csrfToken}
+               </form>
+       </div>
+</div>
+
+{@$form->getHtml()}
+
+{include file='footer'}
index 4d5b28566c1a8b40b3b54a39ea2dc97e7b594580..404dbc7bb114014200f6a01a41930029dbc5c7f8 100755 (executable)
@@ -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 (file)
index 0000000..8ca808d
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+namespace wcf\acp\form;
+
+use wcf\form\AbstractForm;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\HeaderUtil;
+
+/**
+ * Aborts the multi-factor authentication process.
+ *
+ * @author     Tim Duesterhus
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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 (file)
index 0000000..ffa6e55
--- /dev/null
@@ -0,0 +1,180 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\object\type\ObjectType;
+use wcf\data\user\User;
+use wcf\form\AbstractForm;
+use wcf\form\AbstractFormBuilderForm;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\cache\runtime\UserProfileRuntimeCache;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\request\LinkHandler;
+use wcf\system\user\multifactor\IMultifactorMethod;
+use wcf\system\user\multifactor\Setup;
+use wcf\system\WCF;
+use wcf\util\HeaderUtil;
+
+/**
+ * Represents the multi-factor authentication form.
+ *
+ * @author     Tim Duesterhus
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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,
+               ]);
+       }
+}
index b29eeb189893fca058cccc277fca141e6827e469..447b4b4b258118feb08818413eca02e6b4b51083 100644 (file)
@@ -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') {