--- /dev/null
+<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}{*
+*}>
--- /dev/null
+<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}{*
+*}>
+{capture assign='pageTitle'}{lang}wcf.user.security.multifactor.authentication{/lang}{/capture}
+{capture assign='contentTitle'}{lang}wcf.user.security.multifactor.authentication{/lang}{/capture}
+
{capture assign='sidebarLeft'}
<section class="box">
<h2 class="boxTitle">{lang}wcf.user.security.multifactor.methods{/lang}</h2>
use wcf\system\user\authentication\password\algorithm\Bcrypt;
use wcf\system\user\authentication\password\IPasswordAlgorithm;
use wcf\system\user\authentication\password\PasswordAlgorithmManager;
+use wcf\system\user\multifactor\backup\CodeFormField;
use wcf\system\WCF;
/**
*/
private $algorithm;
- private const CHUNKS = 4;
- private const CHUNK_LENGTH = 5;
+ public const CHUNKS = 4;
+ public const CHUNK_LENGTH = 5;
private const USER_ATTEMPTS_PER_HOUR = 5;
$codes = $statement->fetchAll(\PDO::FETCH_ASSOC);
$form->appendChildren([
- TextFormField::create('code')
+ CodeFormField::create()
->label('wcf.user.security.multifactor.backup.code')
+ ->description('wcf.user.security.multifactor.backup.code.description')
->autoFocus()
->required()
->addValidator(new FormFieldValidator('code', function (TextFormField $field) use ($codes, $setup) {
use wcf\system\form\builder\field\validation\FormFieldValidator;
use wcf\system\form\builder\IFormDocument;
use wcf\system\form\builder\TemplateFormNode;
+use wcf\system\user\multifactor\email\CodeFormField;
use wcf\system\WCF;
/**
private const LIFETIME = 10 * 60;
private const REFRESH_AFTER = 2 * 60;
- private const LENGTH = 8;
+ public const LENGTH = 8;
private const USER_ATTEMPTS_PER_TEN_MINUTES = 5;
$emailDomain = substr($address, $atSign + 1);
$form->appendChildren([
- TextFormField::create('code')
+ CodeFormField::create()
->label('wcf.user.security.multifactor.email.code')
->description('wcf.user.security.multifactor.email.code.description', [
'emailDomain' => $emailDomain,
--- /dev/null
+<?php
+namespace wcf\system\user\multifactor;
+
+/**
+ * Provides re-usable helper methods for use in multi-factor authentication.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2020 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\System\User\Multifactor
+ * @since 5.4
+ */
+final class Helper {
+ /**
+ * Generates a stream of digits.
+ */
+ public static function digitStream(): \Iterator {
+ $i = 1;
+ while (true) {
+ yield $i++;
+ if ($i > 9) $i = 0;
+ }
+ }
+}
})),
TextFormField::create('deviceName')
->label('wcf.user.security.multifactor.totp.deviceName')
- ->description('wcf.user.security.multifactor.totp.deviceName.description')
+ ->description('wcf.user.security.multifactor.totp.deviceName.description.setup')
->placeholder('wcf.user.security.multifactor.totp.deviceName.placeholder')
->maximumLength(200),
FormButton::create('submitButton')
$form->appendChildren([
RadioButtonFormField::create('device')
->label('wcf.user.security.multifactor.totp.deviceName')
+ ->description('wcf.user.security.multifactor.totp.deviceName.description.auth')
->objectProperty('deviceID')
->options($deviceOptions)
->value($mostRecentlyUsed['deviceID']),
$form->appendChildren([
CodeFormField::create()
->label('wcf.user.security.multifactor.totp.code')
+ ->description('wcf.user.security.multifactor.totp.code.description')
->autoFocus()
->required()
->addValidator(new FormFieldValidator('code', function (CodeFormField $field) use ($devices, $setup) {
--- /dev/null
+<?php
+namespace wcf\system\user\multifactor\backup;
+use wcf\system\form\builder\field\TDefaultIdFormField;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\user\multifactor\BackupMultifactorMethod;
+use wcf\system\user\multifactor\Helper;
+
+/**
+ * Handles the input of a emergency code.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2020 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\System\User\Multifactor\Backup
+ * @since 5.4
+ */
+class CodeFormField extends TextFormField {
+ use TDefaultIdFormField;
+
+ /**
+ * @var int
+ */
+ protected $chunks;
+
+ /**
+ * @var int
+ */
+ protected $chunkLength;
+
+ /**
+ * @inheritDoc
+ */
+ protected $templateName = '__backupCodeField';
+
+ public function __construct() {
+ $this->chunks(BackupMultifactorMethod::CHUNKS);
+ $this->chunkLength(BackupMultifactorMethod::CHUNK_LENGTH);
+ $this->minimumLength($this->getChunks() * $this->getChunkLength());
+
+ $placeholder = '';
+ $gen = Helper::digitStream();
+ for ($i = 0; $i < BackupMultifactorMethod::CHUNKS; $i++) {
+ for ($j = 0; $j < BackupMultifactorMethod::CHUNK_LENGTH; $j++) {
+ $placeholder .= $gen->current();
+ $gen->next();
+ }
+ $placeholder .= ' ';
+ }
+ $this->placeholder($placeholder);
+ }
+
+ /**
+ * Sets the number of chunks.
+ */
+ public function chunks(int $chunks): self {
+ $this->chunks = $chunks;
+ return $this;
+ }
+
+ /**
+ * Sets the length of a single chunk.
+ */
+ public function chunkLength(int $chunkLength): self {
+ $this->chunkLength = $chunkLength;
+ return $this;
+ }
+
+ /**
+ * Returns the number of chunks.
+ */
+ public function getChunks() {
+ return $this->chunks;
+ }
+
+ /**
+ * Returns the length of a single chunk.
+ */
+ public function getChunkLength() {
+ return $this->chunkLength;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected static function getDefaultId(): string {
+ return 'code';
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\user\multifactor\email;
+use wcf\system\form\builder\field\TDefaultIdFormField;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\user\multifactor\EmailMultifactorMethod;
+use wcf\system\user\multifactor\Helper;
+
+/**
+ * Handles the input of an email code.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2020 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\System\User\Multifactor\Email
+ * @since 5.4
+ */
+class CodeFormField extends TextFormField {
+ use TDefaultIdFormField;
+
+ /**
+ * @inheritDoc
+ */
+ protected $templateName = '__emailCodeField';
+
+ public function __construct() {
+ $this->minimumLength(EmailMultifactorMethod::LENGTH);
+ $this->maximumLength(EmailMultifactorMethod::LENGTH);
+
+ $placeholder = '';
+ $gen = Helper::digitStream();
+ for ($i = 0; $i < $this->getMinimumLength(); $i++) {
+ $placeholder .= $gen->current();
+ $gen->next();
+ }
+ $this->placeholder($placeholder);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected static function getDefaultId(): string {
+ return 'code';
+ }
+}
namespace wcf\system\user\multifactor\totp;
use wcf\system\form\builder\field\TDefaultIdFormField;
use wcf\system\form\builder\field\TextFormField;
+use wcf\system\user\multifactor\Helper;
/**
* Handles the input of a TOTP code.
public function __construct() {
$this->minimumLength(Totp::CODE_LENGTH);
$this->maximumLength(Totp::CODE_LENGTH);
- $this->placeholder("123456");
+
+ $placeholder = '';
+ $gen = Helper::digitStream();
+ for ($i = 0; $i < $this->getMinimumLength(); $i++) {
+ $placeholder .= $gen->current();
+ $gen->next();
+ }
+ $this->placeholder($placeholder);
}
/**
}
// Just .multifactorTotpCode is not specific enough.
-input.multifactorTotpCode {
+input.multifactorTotpCode,
+input.multifactorEmailCode {
font-family: monospace;
font-weight: 600;
font-size: 28px;
}
+input.multifactorBackupCode {
+ font-family: monospace;
+ font-weight: 600;
+ font-size: 18px;
+}
.multifactorTotpNewDevice {
display: flex;
<item name="wcf.user.security.multifactor.totp.devices"><![CDATA[Aktive Smartphones]]></item>
<item name="wcf.user.security.multifactor.totp.error.flood"><![CDATA[Bitte {if LANGUAGE_USE_INFORMAL_VARIANT}versuche es{else}versuchen Sie es{/if} später erneut.]]></item>
<item name="wcf.user.security.multifactor.backup.error.flood"><![CDATA[Bitte {if LANGUAGE_USE_INFORMAL_VARIANT}versuche es{else}versuchen Sie es{/if} später erneut.]]></item>
- <item name="wcf.user.security.multifactor.totp.deviceName.description"><![CDATA[Ein beliebiger Name, der dieses Gerät identifiziert.]]></item>
+ <item name="wcf.user.security.multifactor.totp.deviceName.description.setup"><![CDATA[Ein beliebiger Name, der dieses Gerät identifiziert.]]></item>
<item name="wcf.user.security.multifactor.totp.code.description"><![CDATA[Der durch die Smartphone-App generierte 6-stellige Einmalcode.]]></item>
<item name="wcf.user.security.multifactor.totp.newDevice.description"><![CDATA[<p>Authentifizieren Sie sich mit Hilfe einer App auf Ihrem Smartphone.</p>
<ol class="nativeList">
<item name="wcf.user.security.multifactor.totp.lastDevice"><![CDATA[Wenn Sie ihr Smartphone wechseln möchten, fügen Sie bitte zunächst Ihr neues Smartphone hinzu, bevor Sie Ihr letztes Gerät <strong>{$deviceName}</strong> entfernen. {if LANGUAGE_USE_INFORMAL_VARIANT}Verwende{else}Verwenden Sie{/if} bitte die <a href="{link controller='AccountSecurity'}{/link}">Übersicht in der Benutzerkonto-Sicherheit</a>, wenn {if LANGUAGE_USE_INFORMAL_VARIANT}du{else}Sie{/if} die Mehrfaktor-Authentifizierung deaktivieren {if LANGUAGE_USE_INFORMAL_VARIANT}möchtest{else}möchten{/if}.]]></item>
<item name="wcf.user.security.multifactor.totp.lastDevice.title"><![CDATA[Letztes Gerät]]></item>
<item name="wcf.user.security.multifactor.totp.success.delete"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Dein{else}Ihr{/if} Smartphone <strong>{$deviceName}</strong> wurde erfolgreich entfernt.]]></item>
- <item name="wcf.user.security.multifactor.initialBackup"><![CDATA[<p>Die Mehrfaktor-Authentifizierung ist ab sofort für {if LANGUAGE_USE_INFORMAL_VARIANT}dein{else}Ihr{/if} Benutzerkonto aktiv. {if LANGUAGE_USE_INFORMAL_VARIANT}Du wirst{else}Sie werden{/if} von nun an bei jedem Login den zusätzlichen Faktor benötigen.</p>\r
-<p>Zusätzlich wurden Backup-Codes generiert, mit denen der Zugriff wiederhergestellt werden kann, falls der zusätzliche Faktor unbrauchbar wird.</p>\r
+ <item name="wcf.user.security.multifactor.initialBackup"><![CDATA[<p>Die Mehrfaktor-Authentifizierung ist ab sofort für {if LANGUAGE_USE_INFORMAL_VARIANT}dein{else}Ihr{/if} Benutzerkonto aktiv. {if LANGUAGE_USE_INFORMAL_VARIANT}Du wirst{else}Sie werden{/if} von nun an bei jedem Login den zusätzlichen Faktor benötigen.</p>
+<p>Zusätzlich wurden Backup-Codes generiert, mit denen der Zugriff wiederhergestellt werden kann, falls der zusätzliche Faktor unbrauchbar wird.</p>
<p>Bitte {if LANGUAGE_USE_INFORMAL_VARIANT}Notiere oder speichere dir{else}Notieren oder speichern Sie{/if} sich die unterhalb angezeigten Notfall-Codes. Ein möglicher Aufbewahrungsort könnte ein Blatt Papier in einem Aktenordner sein.</p>]]></item>
<item name="wcf.user.security.multifactor.com.woltlab.wcf.multifactor.email"><![CDATA[Einmalcode über E-Mail]]></item>
<item name="wcf.user.security.multifactor.com.woltlab.wcf.multifactor.email.manage"><![CDATA[Einmalcode über E-Mail]]></item>
<item name="wcf.user.security.multifactor.email.subject"><![CDATA[{$code} ist {if LANGUAGE_USE_INFORMAL_VARIANT}dein{else}ihr{/if} Einmalcode for {@PAGE_TITLE|language}]]></item>
<item name="wcf.user.security.multifactor.email.body.html"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Dein{else}Ihr{/if} Einmalcode lautet: <pre>{$code}</pre>]]></item>
<item name="wcf.user.security.multifactor.email.body.plain"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Dein{else}Ihr{/if} Einmalcode lautet: {$code}]]></item>
+ <item name="wcf.user.security.multifactor.methods"><![CDATA[Verfahren]]></item>
+ <item name="wcf.user.security.multifactor.authentication"><![CDATA[Mehrfaktor-Authentifizierung]]></item>
+ <item name="wcf.user.security.multifactor.backup.code"><![CDATA[Notfall-Code]]></item>
+ <item name="wcf.user.security.multifactor.backup.code.description"><![CDATA[Ein Notfall-Code besteht aus 20 Ziffern und ist nur einmal gültig.]]></item>
+ <item name="wcf.user.security.multifactor.totp.deviceName.description.auth"><![CDATA[Das Gerät, das den genutzten Éinmalcode generiert hat.]]></item>
</category>
<category name="wcf.user.trophy">
<item name="wcf.user.trophy.trophyPoints"><![CDATA[Trophäen]]></item>
<item name="wcf.user.security.multifactor.totp.devices"><![CDATA[Active Smartphones]]></item>
<item name="wcf.user.security.multifactor.totp.error.flood"><![CDATA[Please try again later.]]></item>
<item name="wcf.user.security.multifactor.backup.error.flood"><![CDATA[Please try again later.]]></item>
- <item name="wcf.user.security.multifactor.totp.deviceName.description"><![CDATA[An arbitrary name identifying this device.]]></item>
+ <item name="wcf.user.security.multifactor.totp.deviceName.description.setup"><![CDATA[An arbitrary name identifying this device.]]></item>
<item name="wcf.user.security.multifactor.totp.code.description"><![CDATA[The 6-digit one time code generated by the smartphone app.]]></item>
<item name="wcf.user.security.multifactor.totp.newDevice.description"><![CDATA[<p>Authenticate using an app on your smartphone.</p>
<ol class="nativeList">
<item name="wcf.user.security.multifactor.totp.lastDevice"><![CDATA[Please add your new device before removing your last device <strong>{$deviceName}</strong> if you want to switch phones. Use the <a href="{link controller='AccountSecurity'}{/link}">Overview in Account Security</a> if you want to disable multi-factor authentication.]]></item>
<item name="wcf.user.security.multifactor.totp.lastDevice.title"><![CDATA[Last Device]]></item>
<item name="wcf.user.security.multifactor.totp.success.delete"><![CDATA[Your smartphone <strong>{$deviceName}</strong> has successfully been removed.]]></item>
- <item name="wcf.user.security.multifactor.initialBackup"><![CDATA[<p>The multi-factor authentication is enabled for your account starting now. Going forward you will need to have your second factor handy for every login.</p>\r
-<p>In addition we generated emergency codes for you. They will allow you to gain access to your account in case your second factor becomes unavailable.</p>\r
+ <item name="wcf.user.security.multifactor.initialBackup"><![CDATA[<p>The multi-factor authentication is enabled for your account starting now. Going forward you will need to have your second factor handy for every login.</p>
+<p>In addition we generated emergency codes for you. They will allow you to gain access to your account in case your second factor becomes unavailable.</p>
<p>Please carefully note or save the emergency codes shown below. An example of a secure storage could be a piece of paper within a filing cabinet.</p>]]></item>
<item name="wcf.user.security.multifactor.com.woltlab.wcf.multifactor.email"><![CDATA[Code via Email]]></item>
<item name="wcf.user.security.multifactor.com.woltlab.wcf.multifactor.email.manage"><![CDATA[Code via Email]]></item>
<item name="wcf.user.security.multifactor.email.subject"><![CDATA[{$code} is your one time code for {@PAGE_TITLE|language}]]></item>
<item name="wcf.user.security.multifactor.email.body.html"><![CDATA[Your one time code is: <pre>{$code}</pre>]]></item>
<item name="wcf.user.security.multifactor.email.body.plain"><![CDATA[Your one time code is: {$code}]]></item>
+ <item name="wcf.user.security.multifactor.methods"><![CDATA[Method]]></item>
+ <item name="wcf.user.security.multifactor.authentication"><![CDATA[Multi-Factor Authentication]]></item>
+ <item name="wcf.user.security.multifactor.backup.code"><![CDATA[Emergency Code]]></item>
+ <item name="wcf.user.security.multifactor.backup.code.description"><![CDATA[An emergency code consists of 20 digits and may only be used once.]]></item>
+ <item name="wcf.user.security.multifactor.totp.deviceName.description.auth"><![CDATA[The device that generated the used one time code.]]></item>
</category>
<category name="wcf.user.trophy">
<item name="wcf.user.trophy.trophyPoints"><![CDATA[Trophies]]></item>