From b15436bfdbc15b1607a4fbd56d62d775a04805e3 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 6 Nov 2020 09:38:27 +0100 Subject: [PATCH] Add MultifactorAuthenticationForm --- com.woltlab.wcf/objectType.xml | 4 +- com.woltlab.wcf/page.xml | 11 ++ .../templates/multifactorAuthentication.tpl | 25 +++ .../files/lib/acp/form/LoginForm.class.php | 12 +- .../files/lib/form/LoginForm.class.php | 8 +- .../MultifactorAuthenticationForm.class.php | 152 ++++++++++++++++++ .../BackupMultifactorMethod.class.php | 85 ++++++++++ .../multifactor/IMultifactorMethod.class.php | 24 ++- .../TotpMultifactorMethod.class.php | 18 +++ 9 files changed, 330 insertions(+), 9 deletions(-) create mode 100644 com.woltlab.wcf/templates/multifactorAuthentication.tpl create mode 100644 wcfsetup/install/files/lib/form/MultifactorAuthenticationForm.class.php diff --git a/com.woltlab.wcf/objectType.xml b/com.woltlab.wcf/objectType.xml index 3108650d25..08aa82aec0 100644 --- a/com.woltlab.wcf/objectType.xml +++ b/com.woltlab.wcf/objectType.xml @@ -1725,12 +1725,14 @@ com.woltlab.wcf.multifactor.backup com.woltlab.wcf.multifactor sticky-note + 15 wcf\system\user\multifactor\BackupMultifactorMethod com.woltlab.wcf.multifactor.totp com.woltlab.wcf.multifactor mobile + 10 wcf\system\user\multifactor\TotpMultifactorMethod @@ -1748,7 +1750,7 @@ com.woltlab.wcf.page - + com.woltlab.wcf.rebuildData diff --git a/com.woltlab.wcf/page.xml b/com.woltlab.wcf/page.xml index 0cd257e6f4..46d1750fb6 100644 --- a/com.woltlab.wcf/page.xml +++ b/com.woltlab.wcf/page.xml @@ -838,6 +838,17 @@ E-Mail: [E-Mail-Adresse der verantwortlichen Stelle]


Verantwort 1 1 + + system + wcf\form\MultifactorAuthenticationForm + Multifactor Authentication + Mehrfaktor-Authentifizierung + 1 + com.woltlab.wcf.Login + 1 + 1 + 1 + diff --git a/com.woltlab.wcf/templates/multifactorAuthentication.tpl b/com.woltlab.wcf/templates/multifactorAuthentication.tpl new file mode 100644 index 0000000000..2570189b5f --- /dev/null +++ b/com.woltlab.wcf/templates/multifactorAuthentication.tpl @@ -0,0 +1,25 @@ +{capture assign='sidebarLeft'} +

+

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

+ +
+ +
+
+{/capture} + +{include file='header' __disableAds=true __sidebarLeftHasMenu=true} + +{$user->username} + +{@$form->getHtml()} + +{include file='footer' __disableAds=true} diff --git a/wcfsetup/install/files/lib/acp/form/LoginForm.class.php b/wcfsetup/install/files/lib/acp/form/LoginForm.class.php index 4baeb41c8a..208535a649 100755 --- a/wcfsetup/install/files/lib/acp/form/LoginForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/LoginForm.class.php @@ -5,9 +5,11 @@ use wcf\data\user\authentication\failure\UserAuthenticationFailureAction; use wcf\data\user\User; use wcf\data\user\UserProfile; use wcf\form\AbstractCaptchaForm; +use wcf\form\MultifactorAuthenticationForm; use wcf\system\application\ApplicationHandler; use wcf\system\exception\NamedUserException; use wcf\system\exception\UserInputException; +use wcf\system\request\LinkHandler; use wcf\system\request\RequestHandler; use wcf\system\request\RouteHandler; use wcf\system\user\authentication\EmailUserAuthentication; @@ -200,16 +202,20 @@ class LoginForm extends AbstractCaptchaForm { parent::save(); // change user - WCF::getSession()->changeUser($this->user); + $needsMultifactor = WCF::getSession()->changeUserAfterMultifactor($this->user); $this->saved(); - $this->performRedirect(); + $this->performRedirect($needsMultifactor); } /** * Performs the redirect after successful authentication. */ - protected function performRedirect() { + protected function performRedirect(bool $needsMultifactor = false) { + if ($needsMultifactor) { + $this->url = LinkHandler::getInstance()->getControllerLink(MultifactorAuthenticationForm::class); + } + if (!empty($this->url)) { HeaderUtil::redirect($this->url); } diff --git a/wcfsetup/install/files/lib/form/LoginForm.class.php b/wcfsetup/install/files/lib/form/LoginForm.class.php index f1ec31c455..d2b035d665 100644 --- a/wcfsetup/install/files/lib/form/LoginForm.class.php +++ b/wcfsetup/install/files/lib/form/LoginForm.class.php @@ -24,14 +24,14 @@ class LoginForm extends \wcf\acp\form\LoginForm { if (FORCE_LOGIN) WCF::getSession()->unregister('__wsc_forceLoginRedirect'); // change user - WCF::getSession()->changeUser($this->user); + $needsMultifactor = WCF::getSession()->changeUserAfterMultifactor($this->user); $this->saved(); // redirect to url WCF::getTPL()->assign('__hideUserMenu', true); - $this->performRedirect(); + $this->performRedirect($needsMultifactor); } /** @@ -53,11 +53,11 @@ class LoginForm extends \wcf\acp\form\LoginForm { /** * @inheritDoc */ - protected function performRedirect() { + protected function performRedirect(bool $needsMultifactor = false) { if (empty($this->url) || mb_stripos($this->url, '?login/') !== false || mb_stripos($this->url, '/login/') !== false) { $this->url = LinkHandler::getInstance()->getLink(); } - parent::performRedirect(); + parent::performRedirect($needsMultifactor); } } diff --git a/wcfsetup/install/files/lib/form/MultifactorAuthenticationForm.class.php b/wcfsetup/install/files/lib/form/MultifactorAuthenticationForm.class.php new file mode 100644 index 0000000000..1bd63a6959 --- /dev/null +++ b/wcfsetup/install/files/lib/form/MultifactorAuthenticationForm.class.php @@ -0,0 +1,152 @@ + + * @package WoltLabSuite\Core\Form + * @since 5.4 + */ +class MultifactorAuthenticationForm extends AbstractFormBuilderForm { + const AVAILABLE_DURING_OFFLINE_MODE = true; + + /** + * @inheritDoc + */ + public $formAction = 'authenticate'; + + /** + * @var User + */ + private $user; + + /** + * @var ObjectType[] + */ + private $methods; + + /** + * @var ObjectType + */ + private $method; + + /** + * @var IMultifactorMethod + */ + private $processor; + + /** + * @var int + */ + private $setupId; + + /** + * @inheritDoc + */ + public function readParameters() { + parent::readParameters(); + + $userId = WCF::getSession()->getVar('__changeUserAfterMultifactor__'); + if (!$userId) { + throw new PermissionDeniedException(); + } + $this->user = new User($userId); + if (!$this->user->userID) { + throw new PermissionDeniedException(); + } + + $this->methods = $this->user->getEnabledMultifactorMethods(); + + if (empty($this->methods)) { + throw new \LogicException('Unreachable'); + } + + uasort($this->methods, function (ObjectType $a, ObjectType $b) { + return $b->priority <=> $a->priority; + }); + + $this->setupId = array_keys($this->methods)[0]; + if (isset($_GET['id'])) { + $this->setupId = $_GET['id']; + } + + if (!isset($this->methods[$this->setupId])) { + throw new IllegalLinkException(); + } + + $this->method = $this->methods[$this->setupId]; + 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->setupId); + } + + public function save() { + AbstractForm::save(); + + WCF::getDB()->beginTransaction(); + + $this->returnData = $this->processor->processAuthenticationForm($this->form, $this->setupId); + + WCF::getDB()->commitTransaction(); + + WCF::getSession()->changeUser($this->user); + + $this->saved(); + } + + /** + * @inheritDoc + */ + public function saved() { + AbstractForm::saved(); + + $this->form->cleanup(); + $this->buildForm(); + + // TODO: Proper success message and hiding of the form. + $this->form->showSuccessMessage(true); + } + + /** + * @inheritDoc + */ + protected function setFormAction() { + $this->form->action(LinkHandler::getInstance()->getControllerLink(static::class, [ + 'id' => $this->setupId, + ])); + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign([ + 'method' => $this->method, + 'methods' => $this->methods, + 'user' => $this->user, + 'setupId' => $this->setupId, + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/user/multifactor/BackupMultifactorMethod.class.php b/wcfsetup/install/files/lib/system/user/multifactor/BackupMultifactorMethod.class.php index a783a9b9b1..316d12d852 100644 --- a/wcfsetup/install/files/lib/system/user/multifactor/BackupMultifactorMethod.class.php +++ b/wcfsetup/install/files/lib/system/user/multifactor/BackupMultifactorMethod.class.php @@ -2,6 +2,7 @@ namespace wcf\system\user\multifactor; use wcf\system\form\builder\container\FormContainer; use wcf\system\form\builder\field\ButtonFormField; +use wcf\system\form\builder\field\TextFormField; use wcf\system\form\builder\field\validation\FormFieldValidationError; use wcf\system\form\builder\field\validation\FormFieldValidator; use wcf\system\form\builder\IFormDocument; @@ -171,4 +172,88 @@ class BackupMultifactorMethod implements IMultifactorMethod { return $codes; } + + /** + * Returns a code from $codes matching the $userCode. `null` is returned if + * no matching code could be found. + */ + private function findValidCode(string $userCode, array $codes): ?array { + $manager = PasswordAlgorithmManager::getInstance(); + + $result = null; + foreach ($codes as $code) { + [$algorithmName, $hash] = explode(':', $code['code']); + $algorithm = $manager->getAlgorithmFromName($algorithmName); + + // The use of `&` is intentional to disable the shortcutting logic. + if ($algorithm->verify($userCode, $hash) & $code['useTime'] === null) { + $result = $code; + } + } + + return $result; + } + + /** + * @inheritDoc + */ + public function createAuthenticationForm(IFormDocument $form, int $setupId): void { + $sql = "SELECT * + FROM wcf".WCF_N."_user_multifactor_backup + WHERE setupID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([$setupId]); + $codes = $statement->fetchAll(\PDO::FETCH_ASSOC); + + $form->appendChildren([ + TextFormField::create('code') + ->label('wcf.user.security.multifactor.backup.code') + ->autoFocus() + ->required() + ->addValidator(new FormFieldValidator('code', function (TextFormField $field) use ($codes) { + $userCode = preg_replace('/\s+/', '', $field->getValue()); + + if ($this->findValidCode($userCode, $codes) === null) { + $field->addValidationError(new FormFieldValidationError('invalid')); + } + })), + ]); + } + + /** + * @inheritDoc + */ + public function processAuthenticationForm(IFormDocument $form, int $setupId): void { + $userCode = \preg_replace('/\s+/', '', $form->getData()['data']['code']); + + $sql = "SELECT * + FROM wcf".WCF_N."_user_multifactor_backup + WHERE setupID = ? + FOR UPDATE"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([$setupId]); + $codes = $statement->fetchAll(\PDO::FETCH_ASSOC); + + $usedCode = $this->findValidCode($userCode, $codes); + + if ($usedCode === null) { + throw new \RuntimeException('Unable to find a valid code.'); + } + + $sql = "UPDATE wcf".WCF_N."_user_multifactor_backup + SET useTime = ? + WHERE setupID = ? + AND identifier = ? + AND useTime IS NULL"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + TIME_NOW, + $setupId, + $usedCode['identifier'], + ]); + + if ($statement->getAffectedRows() !== 1) { + throw new \RuntimeException('Unable to invalidate the code.'); + } + } } diff --git a/wcfsetup/install/files/lib/system/user/multifactor/IMultifactorMethod.class.php b/wcfsetup/install/files/lib/system/user/multifactor/IMultifactorMethod.class.php index b68bebb573..6a456d5857 100644 --- a/wcfsetup/install/files/lib/system/user/multifactor/IMultifactorMethod.class.php +++ b/wcfsetup/install/files/lib/system/user/multifactor/IMultifactorMethod.class.php @@ -28,7 +28,7 @@ interface IMultifactorMethod { * Updates the database information based on the data received in the management form. * * This method will be run within a database transaction and must ensure that a valid database - * state is reached. Specifically the multifcaator method MUST be usable after this method + * state is reached. Specifically the multifactor method MUST be usable after this method * finishes successfully. * * An example of an invalid state could be the removal of all multifactor devices. @@ -39,4 +39,26 @@ interface IMultifactorMethod { * @return mixed Opaque data that will be passed as `$returnData` in createManagementForm(). */ public function processManagementForm(IFormDocument $form, int $setupId); + + /** + * Populates the form to authenticate a user with this method. + */ + public function createAuthenticationForm(IFormDocument $form, int $setupId): void; + + /** + * Updates the database information based on the data received in the authentication form. + * + * This method will be run within a database transaction. + * + * This method MUST revalidate the information received from the form in a transaction safe way + * to prevent concurrent use of the same authentication credentials. + * + * An example of such transaction safe use would be invalidating a code by deleting a database row, + * checking the number of affected rows and bailing out if the value is not exactly `1`. + * + * This method MUST throw an Exception if the database state does not match the expected state. + * + * @throws \RuntimeException + */ + public function processAuthenticationForm(IFormDocument $form, int $setupId): void; } diff --git a/wcfsetup/install/files/lib/system/user/multifactor/TotpMultifactorMethod.class.php b/wcfsetup/install/files/lib/system/user/multifactor/TotpMultifactorMethod.class.php index 9ac83165f0..a12a3cef8a 100644 --- a/wcfsetup/install/files/lib/system/user/multifactor/TotpMultifactorMethod.class.php +++ b/wcfsetup/install/files/lib/system/user/multifactor/TotpMultifactorMethod.class.php @@ -1,6 +1,7 @@ appendChild($newDeviceContainer); } + /** + * @inheritDoc + */ public function processManagementForm(IFormDocument $form, int $setupId): void { $formData = $form->getData(); @@ -81,4 +85,18 @@ class TotpMultifactorMethod implements IMultifactorMethod { TIME_NOW, ]); } + + /** + * @inheritDoc + */ + public function createAuthenticationForm(IFormDocument $form, int $setupId): void { + throw new NotImplementedException('TODO'); + } + + /** + * @inheritDoc + */ + public function processAuthenticationForm(IFormDocument $form, int $setupId): void { + throw new NotImplementedException('TODO'); + } } -- 2.20.1