Improve UX when setting up TOTP
authorTim Düsterhus <duesterhus@woltlab.com>
Tue, 10 Nov 2020 14:07:27 +0000 (15:07 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Mon, 16 Nov 2020 16:29:05 +0000 (17:29 +0100)
com.woltlab.wcf/templates/__totpCodeField.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/__totpNewDeviceContainer.tpl
com.woltlab.wcf/templates/__totpSecretField.tpl
wcfsetup/install/files/lib/system/user/multifactor/TotpMultifactorMethod.class.php
wcfsetup/install/files/lib/system/user/multifactor/totp/CodeFormField.class.php
wcfsetup/install/files/style/ui/accountSecurity.scss
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

diff --git a/com.woltlab.wcf/templates/__totpCodeField.tpl b/com.woltlab.wcf/templates/__totpCodeField.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}{*
+*}>
index f2c9db931239ff8e4e168c382ead1c0fb537413a..19b8887a12ef0d7ed01aefc0c89f802c85341e7d 100644 (file)
                {/if}
        {/if}
        
+       {lang}wcf.user.security.multifactor.totp.newDevice.description{/lang}
+       
        <div class="multifactorTotpNewDevice">
                {if $container->getNodeById('secret')->isAvailable()}
-                       {@$container->getNodeById('secret')->getHtml()}
+                       {@$container->getNodeById('secret')->getFieldHtml()}
                {/if}
                
                <div class="multifactorTotpNewDeviceFields">
index eb3fe6d0c5980e4d481f41b4ba5e8d98172005d8..fc32f52215f50147f3d5033991eaceece7892f3f 100644 (file)
@@ -1,7 +1,6 @@
 <div class="totpSecretContainer">
        <input type="hidden" name="{@$field->getPrefixedId()}" value="{$field->getSignedValue()}">
-       <canvas></canvas>
-
+       <canvas></canvas><br>
        <kbd {*
        *}class="totpSecret" {*
        *}data-issuer="{PAGE_TITLE}" {*
index c21156602ee0b04d404e3c30ff804af29370ca66..9ab6969a34bebc0d88432cb877c41fe4d5d0c675 100644 (file)
@@ -58,6 +58,7 @@ class TotpMultifactorMethod implements IMultifactorMethod {
                                SecretFormField::create(),
                                CodeFormField::create()
                                        ->label('wcf.user.security.multifactor.totp.code')
+                                       ->description('wcf.user.security.multifactor.totp.code.description')
                                        ->required()
                                        ->addValidator(new FormFieldValidator('totpSecretValid', function (CodeFormField $field) {
                                                /** @var SecretFormField $secret */
@@ -72,7 +73,9 @@ class TotpMultifactorMethod implements IMultifactorMethod {
                                        })),
                                TextFormField::create('deviceName')
                                        ->label('wcf.user.security.multifactor.totp.deviceName')
-                                       ->placeholder('wcf.user.security.multifactor.totp.deviceName.placeholder'),
+                                       ->description('wcf.user.security.multifactor.totp.deviceName.description')
+                                       ->placeholder('wcf.user.security.multifactor.totp.deviceName.placeholder')
+                                       ->maximumLength(200),
                                FormButton::create('submitButton')
                                        ->label('wcf.global.button.submit')
                                        ->accessKey('s')
index 5427c4a94c6dd17ac38dda411e3c4645e87a25bd..2af016a27433f230dfce43871da923efffb3a455 100644 (file)
@@ -15,6 +15,11 @@ use wcf\system\form\builder\field\TextFormField;
 class CodeFormField extends TextFormField {
        use TDefaultIdFormField;
        
+       /**
+        * @inheritDoc
+        */
+       protected $templateName = '__totpCodeField';
+       
        /**
         * @var ?int
         */
@@ -23,6 +28,7 @@ class CodeFormField extends TextFormField {
        public function __construct() {
                $this->minimumLength(Totp::CODE_LENGTH);
                $this->maximumLength(Totp::CODE_LENGTH);
+               $this->placeholder("123456");
        }
        
        /**
index 476a0804e0de384688e5709aa571ecb89537cb00..23cfe7fec7eaa72cf139d4d1b33e92308c741a65 100644 (file)
        }
 }
 
+// Just .multifactorTotpCode is not specific enough.
+input.multifactorTotpCode {
+       font-family: monospace;
+       font-weight: 600;
+       font-size: 28px;
+}
+
 .multifactorTotpNewDevice {
        display: flex;
+       flex-direction: column;
        
        .totpSecretContainer {
                text-align: center;
-               width: 250px;
-               margin: 0 5px;
                
                canvas {
                        width: 200px;
        .multifactorTotpNewDeviceFields {
                flex: 1 1 auto;
        }
+       
+       @include screen-md-up {
+               flex-direction: row;
+               
+               .totpSecretContainer {
+                       width: 250px;
+                       margin: 0 5px;
+               }
+       }
 }
index e7235e1c9f1676e8c9279a95c36659e433dac523..ef1a9a4fa772f546eb57680ea2818a03ba1003ce 100644 (file)
@@ -4859,6 +4859,14 @@ Die E-Mail-Adresse des neuen Benutzers lautet: {@$user->email}
                <item name="wcf.user.security.multifactor.totp.deviceName.default"><![CDATA[{TIME_NOW|plainTime}]]></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.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">
+<li>{if LANGUAGE_USE_INFORMAL_VARIANT}Installiere{else}Installieren Sie{/if} eine Authentifizierungs-App wie beispielsweise Google Authenticator (<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" class="externalURL">Android</a>, <a href="https://apps.apple.com/app/google-authenticator/id388497605" class="externalURL">iOS</a>) oder Authy (<a href="https://play.google.com/store/apps/details?id=com.authy.authy" class="externalURL">Android</a>, <a href="https://apps.apple.com/app/authy/id494168017" class="externalURL">iOS</a>).</li>
+<li>{if LANGUAGE_USE_INFORMAL_VARIANT}Scanne{else}Scannen Sie{/if} den QR-Code in der App.</li>
+<li>{if LANGUAGE_USE_INFORMAL_VARIANT}Gib{else}Geben Sie{/if} den durch die App generierten 6-stelligen Einmalcode ein.</li>
+</ol>]]></item>
        </category>
        <category name="wcf.user.trophy">
                <item name="wcf.user.trophy.trophyPoints"><![CDATA[Trophäen]]></item>
index 42135f5dd7c1c0a84f12319f0157a5f473093814..a8fee525ee554f3a367276e158174a58078fd18a 100644 (file)
@@ -4856,6 +4856,14 @@ Open the link below to access the user profile:
                <item name="wcf.user.security.multifactor.totp.deviceName.default"><![CDATA[{TIME_NOW|plainTime}]]></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.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">
+<li>Install an authentication app such as Google Authenticator (<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" class="externalURL">Android</a>, <a href="https://apps.apple.com/app/google-authenticator/id388497605" class="externalURL">iOS</a>) or Authy (<a href="https://play.google.com/store/apps/details?id=com.authy.authy" class="externalURL">Android</a>, <a href="https://apps.apple.com/app/authy/id494168017" class="externalURL">iOS</a>).</li>
+<li>Scan the QR code within the app.</li>
+<li>Enter the 6 digit one time code generated by the app.</li>
+</ol>]]></item>
        </category>
        <category name="wcf.user.trophy">
                <item name="wcf.user.trophy.trophyPoints"><![CDATA[Trophies]]></item>