From 775bf303cb30fca24898899d44c2a4819a2369fa Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 6 Nov 2020 12:25:56 +0100 Subject: [PATCH] Add authentication support to TotpMultifactorMethod --- com.woltlab.wcf/objectType.xml | 2 +- .../TotpMultifactorMethod.class.php | 105 +++++++++++++++++- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/com.woltlab.wcf/objectType.xml b/com.woltlab.wcf/objectType.xml index 08aa82aec0..554f04e1df 100644 --- a/com.woltlab.wcf/objectType.xml +++ b/com.woltlab.wcf/objectType.xml @@ -1725,7 +1725,7 @@ com.woltlab.wcf.multifactor.backup com.woltlab.wcf.multifactor sticky-note - 15 + 1 wcf\system\user\multifactor\BackupMultifactorMethod 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 a12a3cef8a..50aa6be021 100644 --- a/wcfsetup/install/files/lib/system/user/multifactor/TotpMultifactorMethod.class.php +++ b/wcfsetup/install/files/lib/system/user/multifactor/TotpMultifactorMethod.class.php @@ -1,14 +1,17 @@ prepareStatement($sql); + $statement->execute([$setupId]); + $devices = $statement->fetchAll(\PDO::FETCH_ASSOC); + + if (count($devices) > 1) { + $deviceOptions = []; + $mostRecentlyUsed = null; + foreach ($devices as $device) { + $deviceOptions[$device['deviceID']] = $device['deviceName']; + + if ($mostRecentlyUsed === null || $mostRecentlyUsed['useTime'] < $device['useTime']) { + $mostRecentlyUsed = $device; + } + } + + $form->appendChildren([ + RadioButtonFormField::create('device') + ->label('wcf.user.security.multifactor.totp.deviceName') + ->objectProperty('deviceID') + ->options($deviceOptions) + ->value($mostRecentlyUsed['deviceID']), + ]); + } + else { + $form->appendChildren([ + HiddenFormField::create('device') + ->objectProperty('deviceID') + ->value($devices[0]['deviceID']), + ]); + } + + $form->appendChildren([ + CodeFormField::create() + ->label('wcf.user.security.multifactor.totp.code') + ->autoFocus() + ->required() + ->addValidator(new FormFieldValidator('code', function (CodeFormField $field) use ($devices) { + /** @var IFormField $deviceField */ + $deviceField = $field->getDocument()->getNodeById('device'); + + $selectedDevice = null; + foreach ($devices as $device) { + if ($device['deviceID'] === $deviceField->getValue()) { + $selectedDevice = $device; + } + } + if ($selectedDevice === null) { + $field->addValidationError(new FormFieldValidationError('invalid')); + } + + $totp = new Totp($selectedDevice['secret']); + $minCounter = $selectedDevice['minCounter']; + if (!$totp->validateTotpCode($field->getValue(), $minCounter, new \DateTime())) { + $field->addValidationError(new FormFieldValidationError('invalid')); + } + $field->minCounter($minCounter); + })), + ]); } /** * @inheritDoc */ public function processAuthenticationForm(IFormDocument $form, int $setupId): void { - throw new NotImplementedException('TODO'); + $formData = $form->getData(); + + $sql = "SELECT * + FROM wcf".WCF_N."_user_multifactor_totp + WHERE setupID = ? + AND deviceID = ? + FOR UPDATE"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + $setupId, + $formData['data']['deviceID'], + ]); + $device = $statement->fetchArray(); + + if ($device === null) { + throw new \RuntimeException('Unable to find the device.'); + } + + $sql = "UPDATE wcf".WCF_N."_user_multifactor_totp + SET useTime = ?, + minCounter = ? + WHERE setupID = ? + AND deviceID = ? + AND minCounter < ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + TIME_NOW, + $formData['data']['code']['minCounter'], + $setupId, + $formData['data']['deviceID'], + $formData['data']['code']['minCounter'], + ]); + + if ($statement->getAffectedRows() !== 1) { + throw new \RuntimeException('Unable to invalidate the code.'); + } } } -- 2.20.1