]> git.immae.eu Git - github/wallabag/wallabag.git/commitdiff
Merge pull request #3798 from wallabag/update-two-factor-bundle
authorKevin Decherf <kevin@kdecherf.com>
Wed, 30 Jan 2019 00:02:27 +0000 (01:02 +0100)
committerGitHub <noreply@github.com>
Wed, 30 Jan 2019 00:02:27 +0000 (01:02 +0100)
Enable OTP 2FA

41 files changed:
.editorconfig
.travis.yml
app/DoctrineMigrations/Version20181202073750.php [new file with mode: 0644]
app/Resources/static/themes/_global/index.js
app/Resources/static/themes/material/index.js
app/config/config.yml
app/config/routing.yml
app/config/security.yml
composer.json
src/Wallabag/CoreBundle/Command/ShowUserCommand.php
src/Wallabag/CoreBundle/Controller/ConfigController.php
src/Wallabag/CoreBundle/Form/Type/UserInformationType.php
src/Wallabag/CoreBundle/Resources/translations/messages.da.yml
src/Wallabag/CoreBundle/Resources/translations/messages.de.yml
src/Wallabag/CoreBundle/Resources/translations/messages.en.yml
src/Wallabag/CoreBundle/Resources/translations/messages.es.yml
src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml
src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
src/Wallabag/CoreBundle/Resources/translations/messages.it.yml
src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml
src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml
src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml
src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml
src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml
src/Wallabag/CoreBundle/Resources/translations/messages.th.yml
src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml
src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig
src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/otp_app.html.twig [new file with mode: 0644]
src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig
src/Wallabag/CoreBundle/Resources/views/themes/material/Config/otp_app.html.twig [new file with mode: 0644]
src/Wallabag/UserBundle/Controller/ManageController.php
src/Wallabag/UserBundle/Entity/User.php
src/Wallabag/UserBundle/Form/UserType.php
src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php
src/Wallabag/UserBundle/Resources/views/Authentication/form.html.twig
src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig
tests/Wallabag/CoreBundle/Command/ShowUserCommandTest.php
tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php
tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php
tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php
tests/Wallabag/UserBundle/Mailer/AuthCodeMailerTest.php

index 6553d30fd52c80bfca71daa162b6812066770126..140440443dbf9e8fdaaf507dbe401455507d8beb 100644 (file)
@@ -13,5 +13,5 @@ insert_final_newline = true
 indent_style = space
 indent_size = 2
 
-[Makefile]
+[*akefile]
 indent_style = tab
index 0ca1e192c49b0d333623b5e78006d6320fb61964..393d00338e0c0d9a6db67eee08f66f0e64df5302 100644 (file)
@@ -51,13 +51,13 @@ install:
 
 before_script:
     - PHP=$TRAVIS_PHP_VERSION
-    - if [[ ! $PHP = hhvm* ]]; then echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi;
-    # xdebug isn't enable for PHP 7.1
-    - if [[ ! $PHP = hhvm* ]]; then phpenv config-rm xdebug.ini || echo "xdebug not available"; fi
+    - echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
+    - phpenv config-rm xdebug.ini || echo "xdebug not available"
     - composer self-update --no-progress
 
 script:
     - travis_wait bash composer install -o --no-interaction --no-progress --prefer-dist
+
     - echo "travis_fold:start:prepare"
     - make prepare DB=$DB
     - echo "travis_fold:end:prepare"
diff --git a/app/DoctrineMigrations/Version20181202073750.php b/app/DoctrineMigrations/Version20181202073750.php
new file mode 100644 (file)
index 0000000..5978291
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+
+namespace Application\Migrations;
+
+use Doctrine\DBAL\Schema\Schema;
+use Wallabag\CoreBundle\Doctrine\WallabagMigration;
+
+/**
+ * Add 2fa OTP stuff.
+ */
+final class Version20181202073750 extends WallabagMigration
+{
+    public function up(Schema $schema): void
+    {
+        switch ($this->connection->getDatabasePlatform()->getName()) {
+            case 'sqlite':
+                $this->addSql('DROP INDEX UNIQ_1D63E7E5C05FB297');
+                $this->addSql('DROP INDEX UNIQ_1D63E7E5A0D96FBF');
+                $this->addSql('DROP INDEX UNIQ_1D63E7E592FC23A8');
+                $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('user', true) . ' AS SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, twoFactorAuthentication FROM ' . $this->getTable('user', true) . '');
+                $this->addSql('DROP TABLE ' . $this->getTable('user', true) . '');
+                $this->addSql('CREATE TABLE ' . $this->getTable('user', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(180) NOT NULL COLLATE BINARY, username_canonical VARCHAR(180) NOT NULL COLLATE BINARY, email VARCHAR(180) NOT NULL COLLATE BINARY, email_canonical VARCHAR(180) NOT NULL COLLATE BINARY, enabled BOOLEAN NOT NULL, password VARCHAR(255) NOT NULL COLLATE BINARY, last_login DATETIME DEFAULT NULL, password_requested_at DATETIME DEFAULT NULL, name CLOB DEFAULT NULL COLLATE BINARY, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, authCode INTEGER DEFAULT NULL, emailTwoFactor BOOLEAN NOT NULL, salt VARCHAR(255) DEFAULT NULL, confirmation_token VARCHAR(180) DEFAULT NULL, roles CLOB NOT NULL --(DC2Type:array)
+                , googleAuthenticatorSecret VARCHAR(255) DEFAULT NULL, backupCodes CLOB DEFAULT NULL --(DC2Type:json_array)
+                )');
+                $this->addSql('INSERT INTO ' . $this->getTable('user', true) . ' (id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, emailTwoFactor) SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, twoFactorAuthentication FROM __temp__' . $this->getTable('user', true) . '');
+                $this->addSql('DROP TABLE __temp__' . $this->getTable('user', true) . '');
+                $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E5C05FB297 ON ' . $this->getTable('user', true) . ' (confirmation_token)');
+                $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E5A0D96FBF ON ' . $this->getTable('user', true) . ' (email_canonical)');
+                $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E592FC23A8 ON ' . $this->getTable('user', true) . ' (username_canonical)');
+                break;
+            case 'mysql':
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD googleAuthenticatorSecret VARCHAR(191) DEFAULT NULL');
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' CHANGE twoFactorAuthentication emailTwoFactor BOOLEAN NOT NULL');
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' DROP trusted');
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD backupCodes LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json_array)\'');
+                break;
+            case 'postgresql':
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD googleAuthenticatorSecret VARCHAR(191) DEFAULT NULL');
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' RENAME COLUMN twofactorauthentication TO emailTwoFactor');
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' DROP trusted');
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD backupCodes TEXT DEFAULT NULL');
+                break;
+        }
+    }
+
+    public function down(Schema $schema): void
+    {
+        switch ($this->connection->getDatabasePlatform()->getName()) {
+            case 'sqlite':
+                $this->addSql('DROP INDEX UNIQ_1D63E7E592FC23A8');
+                $this->addSql('DROP INDEX UNIQ_1D63E7E5A0D96FBF');
+                $this->addSql('DROP INDEX UNIQ_1D63E7E5C05FB297');
+                $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('user', true) . ' AS SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, emailTwoFactor FROM "' . $this->getTable('user', true) . '"');
+                $this->addSql('DROP TABLE "' . $this->getTable('user', true) . '"');
+                $this->addSql('CREATE TABLE "' . $this->getTable('user', true) . '" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(180) NOT NULL, username_canonical VARCHAR(180) NOT NULL, email VARCHAR(180) NOT NULL, email_canonical VARCHAR(180) NOT NULL, enabled BOOLEAN NOT NULL, password VARCHAR(255) NOT NULL, last_login DATETIME DEFAULT NULL, password_requested_at DATETIME DEFAULT NULL, name CLOB DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, authCode INTEGER DEFAULT NULL, twoFactorAuthentication BOOLEAN NOT NULL, salt VARCHAR(255) NOT NULL COLLATE BINARY, confirmation_token VARCHAR(255) DEFAULT NULL COLLATE BINARY, roles CLOB NOT NULL COLLATE BINARY, trusted CLOB DEFAULT NULL COLLATE BINARY)');
+                $this->addSql('INSERT INTO "' . $this->getTable('user', true) . '" (id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, twoFactorAuthentication) SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, emailTwoFactor FROM __temp__' . $this->getTable('user', true) . '');
+                $this->addSql('DROP TABLE __temp__' . $this->getTable('user', true) . '');
+                $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E592FC23A8 ON "' . $this->getTable('user', true) . '" (username_canonical)');
+                $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E5A0D96FBF ON "' . $this->getTable('user', true) . '" (email_canonical)');
+                $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E5C05FB297 ON "' . $this->getTable('user', true) . '" (confirmation_token)');
+                break;
+            case 'mysql':
+                $this->addSql('ALTER TABLE `' . $this->getTable('user') . '` DROP googleAuthenticatorSecret');
+                $this->addSql('ALTER TABLE `' . $this->getTable('user') . '` CHANGE emailtwofactor twoFactorAuthentication BOOLEAN NOT NULL');
+                $this->addSql('ALTER TABLE `' . $this->getTable('user') . '` ADD trusted TEXT DEFAULT NULL');
+                $this->addSql('ALTER TABLE `' . $this->getTable('user') . '` DROP backupCodes');
+                break;
+            case 'postgresql':
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' DROP googleAuthenticatorSecret');
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' RENAME COLUMN emailTwoFactor TO twofactorauthentication');
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD trusted TEXT DEFAULT NULL');
+                $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' DROP backupCodes');
+                break;
+        }
+    }
+}
index bb3e95b6d4bceec69e6e208298a38553d7de9352..9ad96fc020487d888aabd8f330a7351c1767b529 100644 (file)
@@ -89,4 +89,22 @@ $(document).ready(() => {
       }
     };
   });
+
+  // mimic radio button because emailTwoFactor is a boolean
+  $('#update_user_googleTwoFactor').on('change', () => {
+    $('#update_user_emailTwoFactor').prop('checked', false);
+  });
+
+  $('#update_user_emailTwoFactor').on('change', () => {
+    $('#update_user_googleTwoFactor').prop('checked', false);
+  });
+
+  // same mimic for super admin
+  $('#user_googleTwoFactor').on('change', () => {
+    $('#user_emailTwoFactor').prop('checked', false);
+  });
+
+  $('#user_emailTwoFactor').on('change', () => {
+    $('#user_googleTwoFactor').prop('checked', false);
+  });
 });
index 0579459778266ea0e93847af721a86bf946c1de0..2926cad11c1d55fdaacd2af0054fe0b140e56f36 100755 (executable)
@@ -50,25 +50,30 @@ $(document).ready(() => {
     $('#tag_label').focus();
     return false;
   });
+
   $('#nav-btn-add').on('click', () => {
     toggleNav('.nav-panel-add', '#entry_url');
     return false;
   });
+
   const materialAddForm = $('.nav-panel-add');
   materialAddForm.on('submit', () => {
     materialAddForm.addClass('disabled');
     $('input#entry_url', materialAddForm).prop('readonly', true).trigger('blur');
   });
+
   $('#nav-btn-search').on('click', () => {
     toggleNav('.nav-panel-search', '#search_entry_term');
     return false;
   });
+
   $('.close').on('click', (e) => {
     $(e.target).parent('.nav-panel-item').hide(100);
     $('.nav-panel-actions').show(100);
     $('.nav-panels').css('background', 'transparent');
     return false;
   });
+
   $(window).scroll(() => {
     const s = $(window).scrollTop();
     const d = $(document).height();
index 4b34af3023c5e4ef2c7e07571c637c4c790fb248..2d8f9bf01eae24a7c1e7dd243f9e42179bd11e73 100644 (file)
@@ -198,10 +198,17 @@ fos_oauth_server:
             refresh_token_lifetime: 1209600
 
 scheb_two_factor:
-    trusted_computer:
+    trusted_device:
         enabled: true
         cookie_name: wllbg_trusted_computer
-        cookie_lifetime: 2592000
+        lifetime: 2592000
+
+    backup_codes:
+        enabled: "%twofactor_auth%"
+
+    google:
+        enabled: "%twofactor_auth%"
+        template: WallabagUserBundle:Authentication:form.html.twig
 
     email:
         enabled: "%twofactor_auth%"
index 0bd2d130675140d54ee3f9c7d4d5b1acb20553e7..a7c0f7e9d17d7c9efe1247df5542d03577049295 100644 (file)
@@ -51,3 +51,11 @@ craue_config_settings_modify:
 
 fos_js_routing:
     resource: "@FOSJsRoutingBundle/Resources/config/routing/routing.xml"
+
+2fa_login:
+    path: /2fa
+    defaults:
+        _controller: "scheb_two_factor.form_controller:form"
+
+2fa_login_check:
+    path: /2fa_check
index 96489e2683f1fcc6aad294b1578108b970c93a9c..6a21b4e557c12cbf53fdab635500bfbe452c8ae3 100644 (file)
@@ -56,9 +56,17 @@ security:
                 path:   /logout
                 target: /
 
+            two_factor:
+                provider: fos_userbundle
+                auth_form_path: 2fa_login
+                check_path: 2fa_login_check
+
     access_control:
         - { path: ^/api/(doc|version|info|user), roles: IS_AUTHENTICATED_ANONYMOUSLY }
         - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
+        # force role for logout otherwise when 2fa enable, you won't be able to logout
+        # https://github.com/scheb/two-factor-bundle/issues/168#issuecomment-430822478
+        - { path: ^/logout, roles: [IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_2FA_IN_PROGRESS] }
         - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
         - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
         - { path: /(unread|starred|archive|all).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
@@ -67,5 +75,6 @@ security:
         - { path: ^/share, roles: IS_AUTHENTICATED_ANONYMOUSLY }
         - { path: ^/settings, roles: ROLE_SUPER_ADMIN }
         - { path: ^/annotations, roles: ROLE_USER }
+        - { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
         - { path: ^/users, roles: ROLE_SUPER_ADMIN }
         - { path: ^/, roles: ROLE_USER }
index 21d71b74a320c0fd2bfc7e1cf800135ae3c845fd..7678d7b87b695b4380507b9eb5fdc430c8b9c356 100644 (file)
@@ -31,7 +31,7 @@
         "issues": "https://github.com/wallabag/wallabag/issues"
     },
     "require": {
-        "php": ">=7.1.0",
+        "php": ">=7.1.3",
         "ext-pcre": "*",
         "ext-dom": "*",
         "ext-curl": "*",
@@ -70,7 +70,7 @@
         "friendsofsymfony/user-bundle": "2.0.*",
         "friendsofsymfony/oauth-server-bundle": "^1.5",
         "stof/doctrine-extensions-bundle": "^1.2",
-        "scheb/two-factor-bundle": "^2.14",
+        "scheb/two-factor-bundle": "^3.0",
         "grandt/phpepub": "dev-master",
         "wallabag/php-mobi": "~1.0",
         "kphoen/rulerz-bundle": "~0.13",
@@ -87,7 +87,8 @@
         "friendsofsymfony/jsrouting-bundle": "^2.2",
         "bdunogier/guzzle-site-authenticator": "^1.0.0",
         "defuse/php-encryption": "^2.1",
-        "html2text/html2text": "^4.1"
+        "html2text/html2text": "^4.1",
+        "pragmarx/recovery": "^0.1.0"
     },
     "require-dev": {
         "doctrine/doctrine-fixtures-bundle": "~3.0",
     "config": {
         "bin-dir": "bin",
         "platform": {
-            "php": "7.1"
+            "php": "7.1.3"
         }
     },
     "minimum-stability": "dev",
index a0184267e8fd632af356e068e956f73dfaabd04a..c95efbf3acb10ba3964267c2344d8e0393058ac6 100644 (file)
@@ -57,7 +57,8 @@ class ShowUserCommand extends ContainerAwareCommand
             sprintf('Display name: %s', $user->getName()),
             sprintf('Creation date: %s', $user->getCreatedAt()->format('Y-m-d H:i:s')),
             sprintf('Last login: %s', null !== $user->getLastLogin() ? $user->getLastLogin()->format('Y-m-d H:i:s') : 'never'),
-            sprintf('2FA activated: %s', $user->isTwoFactorAuthentication() ? 'yes' : 'no'),
+            sprintf('2FA (email) activated: %s', $user->isEmailTwoFactor() ? 'yes' : 'no'),
+            sprintf('2FA (OTP) activated: %s', $user->isGoogleAuthenticatorEnabled() ? 'yes' : 'no'),
         ]);
     }
 
index be6feb7cdd21b441229e72a26813f85e37b69c53..9257ab18df6ad092422e4003701195cf84c9d0c9 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Wallabag\CoreBundle\Controller;
 
+use PragmaRX\Recovery\Recovery as BackupCodes;
 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -46,7 +47,7 @@ class ConfigController extends Controller
             $activeTheme = $this->get('liip_theme.active_theme');
             $activeTheme->setName($config->getTheme());
 
-            $this->get('session')->getFlashBag()->add(
+            $this->addFlash(
                 'notice',
                 'flashes.config.notice.config_saved'
             );
@@ -68,7 +69,7 @@ class ConfigController extends Controller
                 $userManager->updateUser($user, true);
             }
 
-            $this->get('session')->getFlashBag()->add('notice', $message);
+            $this->addFlash('notice', $message);
 
             return $this->redirect($this->generateUrl('config') . '#set4');
         }
@@ -83,7 +84,7 @@ class ConfigController extends Controller
         if ($userForm->isSubmitted() && $userForm->isValid()) {
             $userManager->updateUser($user, true);
 
-            $this->get('session')->getFlashBag()->add(
+            $this->addFlash(
                 'notice',
                 'flashes.config.notice.user_updated'
             );
@@ -99,7 +100,7 @@ class ConfigController extends Controller
             $em->persist($config);
             $em->flush();
 
-            $this->get('session')->getFlashBag()->add(
+            $this->addFlash(
                 'notice',
                 'flashes.config.notice.rss_updated'
             );
@@ -131,7 +132,7 @@ class ConfigController extends Controller
             $em->persist($taggingRule);
             $em->flush();
 
-            $this->get('session')->getFlashBag()->add(
+            $this->addFlash(
                 'notice',
                 'flashes.config.notice.tagging_rules_updated'
             );
@@ -153,11 +154,123 @@ class ConfigController extends Controller
             ],
             'twofactor_auth' => $this->getParameter('twofactor_auth'),
             'wallabag_url' => $this->getParameter('domain_name'),
-            'enabled_users' => $this->get('wallabag_user.user_repository')
-                ->getSumEnabledUsers(),
+            'enabled_users' => $this->get('wallabag_user.user_repository')->getSumEnabledUsers(),
         ]);
     }
 
+    /**
+     * Enable 2FA using email.
+     *
+     * @Route("/config/otp/email", name="config_otp_email")
+     */
+    public function otpEmailAction()
+    {
+        if (!$this->getParameter('twofactor_auth')) {
+            return $this->createNotFoundException('two_factor not enabled');
+        }
+
+        $user = $this->getUser();
+
+        $user->setGoogleAuthenticatorSecret(null);
+        $user->setBackupCodes(null);
+        $user->setEmailTwoFactor(true);
+
+        $this->container->get('fos_user.user_manager')->updateUser($user, true);
+
+        $this->addFlash(
+            'notice',
+            'flashes.config.notice.otp_enabled'
+        );
+
+        return $this->redirect($this->generateUrl('config') . '#set3');
+    }
+
+    /**
+     * Enable 2FA using OTP app, user will need to confirm the generated code from the app.
+     *
+     * @Route("/config/otp/app", name="config_otp_app")
+     */
+    public function otpAppAction()
+    {
+        if (!$this->getParameter('twofactor_auth')) {
+            return $this->createNotFoundException('two_factor not enabled');
+        }
+
+        $user = $this->getUser();
+        $secret = $this->get('scheb_two_factor.security.google_authenticator')->generateSecret();
+
+        $user->setGoogleAuthenticatorSecret($secret);
+        $user->setEmailTwoFactor(false);
+
+        $backupCodes = (new BackupCodes())->toArray();
+        $backupCodesHashed = array_map(
+            function ($backupCode) {
+                return password_hash($backupCode, PASSWORD_DEFAULT);
+            },
+            $backupCodes
+        );
+
+        $user->setBackupCodes($backupCodesHashed);
+
+        $this->container->get('fos_user.user_manager')->updateUser($user, true);
+
+        return $this->render('WallabagCoreBundle:Config:otp_app.html.twig', [
+            'backupCodes' => $backupCodes,
+            'qr_code' => $this->get('scheb_two_factor.security.google_authenticator')->getQRContent($user),
+        ]);
+    }
+
+    /**
+     * Cancelling 2FA using OTP app.
+     *
+     * @Route("/config/otp/app/cancel", name="config_otp_app_cancel")
+     */
+    public function otpAppCancelAction()
+    {
+        if (!$this->getParameter('twofactor_auth')) {
+            return $this->createNotFoundException('two_factor not enabled');
+        }
+
+        $user = $this->getUser();
+        $user->setGoogleAuthenticatorSecret(null);
+        $user->setBackupCodes(null);
+
+        $this->container->get('fos_user.user_manager')->updateUser($user, true);
+
+        return $this->redirect($this->generateUrl('config') . '#set3');
+    }
+
+    /**
+     * Validate OTP code.
+     *
+     * @param Request $request
+     *
+     * @Route("/config/otp/app/check", name="config_otp_app_check")
+     */
+    public function otpAppCheckAction(Request $request)
+    {
+        $isValid = $this->get('scheb_two_factor.security.google_authenticator')->checkCode(
+            $this->getUser(),
+            $request->get('_auth_code')
+        );
+
+        if (true === $isValid) {
+            $this->addFlash(
+                'notice',
+                'flashes.config.notice.otp_enabled'
+            );
+
+            return $this->redirect($this->generateUrl('config') . '#set3');
+        }
+
+        $this->addFlash(
+            'two_factor',
+            'scheb_two_factor.code_invalid'
+        );
+
+        return $this->redirect($this->generateUrl('config_otp_app'));
+    }
+
     /**
      * @param Request $request
      *
@@ -178,7 +291,7 @@ class ConfigController extends Controller
             return new JsonResponse(['token' => $config->getRssToken()]);
         }
 
-        $this->get('session')->getFlashBag()->add(
+        $this->addFlash(
             'notice',
             'flashes.config.notice.rss_token_updated'
         );
@@ -203,7 +316,7 @@ class ConfigController extends Controller
         $em->remove($rule);
         $em->flush();
 
-        $this->get('session')->getFlashBag()->add(
+        $this->addFlash(
             'notice',
             'flashes.config.notice.tagging_rules_deleted'
         );
@@ -269,7 +382,7 @@ class ConfigController extends Controller
                 break;
         }
 
-        $this->get('session')->getFlashBag()->add(
+        $this->addFlash(
             'notice',
             'flashes.config.notice.' . $type . '_reset'
         );
index 07c9994968cee3a8c0122f21b02b0dedc0088831..6e4c9154c5777a39b6ddd65153f507f54f90ddd2 100644 (file)
@@ -21,9 +21,14 @@ class UserInformationType extends AbstractType
             ->add('email', EmailType::class, [
                 'label' => 'config.form_user.email_label',
             ])
-            ->add('twoFactorAuthentication', CheckboxType::class, [
+            ->add('emailTwoFactor', CheckboxType::class, [
                 'required' => false,
-                'label' => 'config.form_user.twoFactorAuthentication_label',
+                'label' => 'config.form_user.emailTwoFactor_label',
+            ])
+            ->add('googleTwoFactor', CheckboxType::class, [
+                'required' => false,
+                'label' => 'config.form_user.googleTwoFactor_label',
+                'mapped' => false,
             ])
             ->add('save', SubmitType::class, [
                 'label' => 'config.form.save',
index 5a770dffb810a85d130d2f50b154dfea8129bc77..454f547dee58fd2d6baf88f7214d5d4b9a654b5e 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'Adgangskode'
         # rules: 'Tagging rules'
         new_user: 'Tilføj bruger'
+        # reset: 'Reset area'
     form:
         save: 'Gem'
     form_settings:
@@ -98,11 +99,19 @@ config:
             # all: 'All'
         # rss_limit: 'Number of items in the feed'
     form_user:
-        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code on every new untrusted connexion"
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'Navn'
         email_label: 'Emailadresse'
-        # twoFactorAuthentication_label: 'Two factor authentication'
-        # help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by email."
+        two_factor:
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             # title: Delete my account (a.k.a danger zone)
             # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out.
@@ -160,6 +169,15 @@ config:
         #         and: 'One rule AND another'
         #         matches: 'Tests that a <i>subject</i> matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>'
         #         notmatches: 'Tests that a <i>subject</i> doesn''t match match a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
+    otp:
+        # page_title: Two-factor authentication
+        # app:
+        #     two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+        #     two_factor_code_description_2: 'You can scan that QR Code with your app:'
+        #     two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+        #     two_factor_code_description_4: 'Test an OTP code from your configured app:'
+        #     cancel: Cancel
+        #     enable: Enable
 
 entry:
     # default_title: 'Title of the entry'
@@ -532,7 +550,8 @@ user:
         email_label: 'Emailadresse'
         # enabled_label: 'Enabled'
         # last_login_label: 'Last login'
-        # twofactor_label: Two factor authentication
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         # save: Save
         # delete: Delete
         # delete_confirm: Are you sure?
index 2ae8f08ecae9b5bd3c8acf217c500e8b744d695f..dc1d4723f65f9d4380c58eebe170b7901a8d5b11 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'Kennwort'
         rules: 'Tagging-Regeln'
         new_user: 'Benutzer hinzufügen'
+        reset: 'Zurücksetzen'
     form:
         save: 'Speichern'
     form_settings:
@@ -98,11 +99,19 @@ config:
             all: 'Alle'
         rss_limit: 'Anzahl der Einträge pro Feed'
     form_user:
-        two_factor_description: "Wenn du die Zwei-Faktor-Authentifizierung aktivierst, erhältst du eine E-Mail mit einem Code bei jeder nicht vertrauenswürdigen Verbindung"
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'Name'
         email_label: 'E-Mail-Adresse'
-        twoFactorAuthentication_label: 'Zwei-Faktor-Authentifizierung'
-        help_twoFactorAuthentication: "Wenn du 2FA aktivierst, wirst du bei jedem Login einen Code per E-Mail bekommen."
+        two_factor:
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             title: 'Lösche mein Konto (a.k.a Gefahrenzone)'
             description: 'Wenn du dein Konto löschst, werden ALL deine Artikel, ALL deine Tags, ALL deine Anmerkungen und dein Konto dauerhaft gelöscht (kann NICHT RÜCKGÄNGIG gemacht werden). Du wirst anschließend ausgeloggt.'
@@ -532,7 +541,8 @@ user:
         email_label: 'E-Mail-Adresse'
         enabled_label: 'Aktiviert'
         last_login_label: 'Letzter Login'
-        twofactor_label: 'Zwei-Faktor-Authentifizierung'
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         save: 'Speichern'
         delete: 'Löschen'
         delete_confirm: 'Bist du sicher?'
index d1d7415913c998dc983aa50bd87a5d15ec16f4be..45145c8069061d3400822136f6b8d760e0e4b8f8 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'Password'
         rules: 'Tagging rules'
         new_user: 'Add a user'
+        reset: 'Reset area'
     form:
         save: 'Save'
     form_settings:
@@ -98,11 +99,19 @@ config:
             all: 'All'
         rss_limit: 'Number of items in the feed'
     form_user:
-        two_factor_description: "Enabling two factor authentication means you'll receive an email with a code on every new untrusted connection."
+        two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'Name'
         email_label: 'Email'
-        twoFactorAuthentication_label: 'Two factor authentication'
-        help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by email."
+        two_factor:
+            emailTwoFactor_label: 'Using email (receive a code by email)'
+            googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            table_method: Method
+            table_state: State
+            table_action: Action
+            state_enabled: Enabled
+            state_disabled: Disabled
+            action_email: Use email
+            action_app: Use OTP App
         delete:
             title: Delete my account (a.k.a danger zone)
             description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out.
@@ -160,6 +169,15 @@ config:
                 and: 'One rule AND another'
                 matches: 'Tests that a <i>subject</i> matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>'
                 notmatches: 'Tests that a <i>subject</i> doesn''t match match a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
+    otp:
+        page_title: Two-factor authentication
+        app:
+            two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+            two_factor_code_description_2: 'You can scan that QR Code with your app:'
+            two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+            two_factor_code_description_4: 'Test an OTP code from your configured app:'
+            cancel: Cancel
+            enable: Enable
 
 entry:
     default_title: 'Title of the entry'
@@ -532,7 +550,8 @@ user:
         email_label: 'Email'
         enabled_label: 'Enabled'
         last_login_label: 'Last login'
-        twofactor_label: Two factor authentication
+        twofactor_email_label: Two factor authentication by email
+        twofactor_google_label: Two factor authentication by OTP app
         save: Save
         delete: Delete
         delete_confirm: Are you sure?
@@ -578,6 +597,7 @@ flashes:
             tags_reset: Tags reset
             entries_reset: Entries reset
             archived_reset: Archived entries deleted
+            otp_enabled: Two-factor authentication enabled
     entry:
         notice:
             entry_already_saved: 'Entry already saved on %date%'
index 741d3e9f3f3f847df5b407939306f5591f0ac2e7..c1047e55a728849c3a7396a1676f8be2f1d93672 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'Contraseña'
         rules: 'Reglas de etiquetado automáticas'
         new_user: 'Añadir un usuario'
+        reset: 'Reiniciar mi cuenta'
     form:
         save: 'Guardar'
     form_settings:
@@ -98,11 +99,19 @@ config:
             # all: 'All'
         rss_limit: 'Límite de artículos en feed RSS'
     form_user:
-        two_factor_description: "Con la autenticación en dos pasos recibirá código por e-mail en cada nueva conexión que no sea de confianza."
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'Nombre'
         email_label: 'Dirección de e-mail'
-        twoFactorAuthentication_label: 'Autenticación en dos pasos'
-        help_twoFactorAuthentication: "Si activas la autenticación en dos pasos, cada vez que quieras iniciar sesión en wallabag recibirás un código por e-mail."
+        two_factor:
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             title: Eliminar mi cuenta (Zona peligrosa)
             description: Si eliminas tu cuenta, TODOS tus artículos, TODAS tus etiquetas, TODAS tus anotaciones y tu cuenta serán eliminadas de forma PERMANENTE (no se puede deshacer). Después serás desconectado.
@@ -160,6 +169,15 @@ config:
                 and: 'Una regla Y la otra'
                 matches: 'Prueba si un <i>sujeto</i> corresponde a una <i>búsqueda</i> (insensible a mayusculas).<br />Ejemplo : <code>title matches "fútbol"</code>'
                 # notmatches: 'Tests that a <i>subject</i> doesn''t match match a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
+    otp:
+        # page_title: Two-factor authentication
+        # app:
+        #     two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+        #     two_factor_code_description_2: 'You can scan that QR Code with your app:'
+        #     two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+        #     two_factor_code_description_4: 'Test an OTP code from your configured app:'
+        #     cancel: Cancel
+        #     enable: Enable
 
 entry:
     default_title: 'Título del artículo'
@@ -532,7 +550,8 @@ user:
         email_label: 'E-mail'
         enabled_label: 'Activado'
         last_login_label: 'Último inicio de sesión'
-        twofactor_label: Autenticación en dos pasos
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         save: Guardar
         delete: Eliminar
         delete_confirm: ¿Estás seguro?
index 2ef5dd528f40b0fc5f06b185612090e8261c3f64..3042de2ef01b1633aa56f352e52860cf3d8a1cfc 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'رمز'
         rules: 'برچسب‌گذاری خودکار'
         new_user: 'افزودن کاربر'
+        # reset: 'Reset area'
     form:
         save: 'ذخیره'
     form_settings:
@@ -98,11 +99,19 @@ config:
             # all: 'All'
         rss_limit: 'محدودیت آر-اس-اس'
     form_user:
-        two_factor_description: "با فعال‌کردن تأیید ۲مرحله‌ای هر بار که اتصال تأییدنشده‌ای برقرار شد، به شما یک کد از راه ایمیل فرستاده می‌شود"
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'نام'
         email_label: 'نشانی ایمیل'
-        twoFactorAuthentication_label: 'تأیید ۲مرحله‌ای'
-        # help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by email."
+        two_factor:
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             # title: Delete my account (a.k.a danger zone)
             # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out.
@@ -160,6 +169,15 @@ config:
         #         and: 'One rule AND another'
         #         matches: 'Tests that a <i>subject</i> matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>'
         #         notmatches: 'Tests that a <i>subject</i> doesn''t match match a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
+    otp:
+        # page_title: Two-factor authentication
+        # app:
+        #     two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+        #     two_factor_code_description_2: 'You can scan that QR Code with your app:'
+        #     two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+        #     two_factor_code_description_4: 'Test an OTP code from your configured app:'
+        #     cancel: Cancel
+        #     enable: Enable
 
 entry:
     # default_title: 'Title of the entry'
@@ -532,7 +550,8 @@ user:
         email_label: 'نشانی ایمیل'
         # enabled_label: 'Enabled'
         # last_login_label: 'Last login'
-        # twofactor_label: Two factor authentication
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         # save: Save
         # delete: Delete
         # delete_confirm: Are you sure?
index 7a2029b45659ca754894be86ac3e2fa3289e1991..57740ba235042d9bf7ade683a2a2afa1d4161c29 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: "Mot de passe"
         rules: "Règles de tag automatiques"
         new_user: "Créer un compte"
+        reset: "Réinitialisation"
     form:
         save: "Enregistrer"
     form_settings:
@@ -98,11 +99,19 @@ config:
             all: "Tous"
         rss_limit: "Nombre d’articles dans le flux"
     form_user:
-        two_factor_description: "Activer l’authentification double-facteur veut dire que vous allez recevoir un code par courriel à chaque nouvelle connexion non approuvée."
+        two_factor_description: "Activer l’authentification double-facteur veut dire que vous allez recevoir un code par courriel OU que vous devriez utiliser une application de mot de passe à usage unique (comme Google Authenticator, Authy or FreeOTP) pour obtenir un code temporaire à chaque nouvelle connexion non approuvée. Vous ne pouvez pas choisir les deux options."
         name_label: "Nom"
         email_label: "Adresse courriel"
-        twoFactorAuthentication_label: "Double authentification"
-        help_twoFactorAuthentication: "Si vous activez 2FA, à chaque tentative de connexion à wallabag, vous recevrez un code par email."
+        two_factor:
+            emailTwoFactor_label: 'En utlisant l’email (recevez un code par email)'
+            googleTwoFactor_label: 'En utilisant une application de mot de passe à usage unique (ouvrez l’app, comme Google Authenticator, Authy or FreeOTP, pour obtenir un mot de passe à usage unique)'
+            table_method: Méthode
+            table_state: État
+            table_action: Action
+            state_enabled: Activé
+            state_disabled: Désactivé
+            action_email: Utiliser l'email
+            action_app: Utiliser une app OTP
         delete:
             title: "Supprimer mon compte (attention danger !)"
             description: "Si vous confirmez la suppression de votre compte, TOUS les articles, TOUS les tags, TOUTES les annotations et votre compte seront DÉFINITIVEMENT supprimé (c’est IRRÉVERSIBLE). Vous serez ensuite déconnecté."
@@ -160,6 +169,15 @@ config:
                 and: "Une règle ET l’autre"
                 matches: "Teste si un <i>sujet</i> correspond à une <i>recherche</i> (non sensible à la casse).<br />Exemple : <code>title matches \"football\"</code>"
                 notmatches: "Teste si un <i>sujet</i> ne correspond pas à une <i>recherche</i> (non sensible à la casse).<br />Exemple : <code>title notmatches \"football\"</code>"
+    otp:
+        page_title: Authentification double-facteur
+        app:
+            two_factor_code_description_1: Vous venez d’activer l’authentification double-facteur, ouvrez votre application OTP pour configurer la génération du mot de passe à usage unique. Ces informations disparaîtront après un rechargement de la page.
+            two_factor_code_description_2: 'Vous pouvez scanner le QR code avec votre application :'
+            two_factor_code_description_3: 'N’oubliez pas de sauvegarder ces codes de secours dans un endroit sûr, vous pourrez les utiliser si vous ne pouvez plus accéder à votre application OTP :'
+            two_factor_code_description_4: 'Testez un code généré par votre application OTP :'
+            cancel: Annuler
+            enable: Activer
 
 entry:
     default_title: "Titre de l’article"
@@ -533,6 +551,8 @@ user:
         enabled_label: "Activé"
         last_login_label: "Dernière connexion"
         twofactor_label: "Double authentification"
+        twofactor_email_label: Double authentification par email
+        twofactor_google_label: Double authentification par OTP app
         save: "Sauvegarder"
         delete: "Supprimer"
         delete_confirm: "Êtes-vous sûr ?"
@@ -578,6 +598,7 @@ flashes:
             tags_reset: "Tags supprimés"
             entries_reset: "Articles supprimés"
             archived_reset: "Articles archivés supprimés"
+            otp_enabled: "Authentification à double-facteur activée"
     entry:
         notice:
             entry_already_saved: "Article déjà sauvegardé le %date%"
index 3a45944555bb63e7baa584df65eb64f202cd1661..274e5338a50f44967bbb9d245707b02ce14f1712 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'Password'
         rules: 'Regole di etichettatura'
         new_user: 'Aggiungi utente'
+        reset: 'Area di reset'
     form:
         save: 'Salva'
     form_settings:
@@ -98,11 +99,18 @@ config:
             # all: 'All'
         rss_limit: 'Numero di elementi nel feed'
     form_user:
-        two_factor_description: "Abilitando l'autenticazione a due fattori riceverai una e-mail con un codice per ogni nuova connesione non verificata"
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'Nome'
         email_label: 'E-mail'
-        twoFactorAuthentication_label: 'Autenticazione a due fattori'
-        help_twoFactorAuthentication: "Se abiliti l'autenticazione a due fattori, ogni volta che vorrai connetterti a wallabag, riceverai un codice via E-mail."
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             title: Cancella il mio account (zona pericolosa)
             description: Rimuovendo il tuo account, TUTTI i tuoi articoli, TUTTE le tue etichette, TUTTE le tue annotazioni ed il tuo account verranno rimossi PERMANENTEMENTE (impossibile da ANNULLARE). Verrai poi disconnesso.
@@ -160,6 +168,15 @@ config:
                 and: "Una regola E un'altra"
                 matches: 'Verifica che un <i>oggetto</i> risulti in una <i>ricerca</i> (case-insensitive).<br />Esempio: <code>titolo contiene "football"</code>'
                 # notmatches: 'Tests that a <i>subject</i> doesn''t match match a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
+    otp:
+        # page_title: Two-factor authentication
+        # app:
+        #     two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+        #     two_factor_code_description_2: 'You can scan that QR Code with your app:'
+        #     two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+        #     two_factor_code_description_4: 'Test an OTP code from your configured app:'
+        #     cancel: Cancel
+        #     enable: Enable
 
 entry:
     default_title: "Titolo del contenuto"
@@ -532,7 +549,8 @@ user:
         email_label: 'E-mail'
         enabled_label: 'Abilitato'
         last_login_label: 'Ultima connessione'
-        twofactor_label: Autenticazione a due fattori
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         save: Salva
         delete: Cancella
         delete_confirm: Sei sicuro?
index 9df9e64561cc1cf025d7cc91b6c3d22908deecf7..4e5370f9884513d311326bd2a3ffcbb5f962961e 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'Senhal'
         rules: "Règlas d'etiquetas automaticas"
         new_user: 'Crear un compte'
+        reset: 'Zòna de reïnicializacion'
     form:
         save: 'Enregistrar'
     form_settings:
@@ -98,11 +99,18 @@ config:
             all: 'Totes'
         rss_limit: "Nombre d'articles dins un flux RSS"
     form_user:
-        two_factor_description: "Activar l'autentificacion en dos temps vòl dire que recebretz un còdi per corrièl per cada novèla connexion pas aprovada."
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'Nom'
         email_label: 'Adreça de corrièl'
-        twoFactorAuthentication_label: 'Dobla autentificacion'
-        help_twoFactorAuthentication: "S'avètz activat l'autentificacion en dos temps, cada còp que volètz vos connectar a wallabag, recebretz un còdi per corrièl."
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             title: Suprimir mon compte (Mèfi zòna perilhosa)
             description: Se confirmatz la supression de vòstre compte, TOTES vòstres articles, TOTAS vòstras etiquetas, TOTAS vòstras anotacions e vòstre compte seràn suprimits per totjorn. E aquò es IRREVERSIBLE. Puèi seretz desconnectat.
@@ -160,6 +168,15 @@ config:
                 and: "Una règla E l'autra"
                 matches: 'Teste se un <i>subjècte</i> correspond a una <i>recèrca</i> (non sensibla a la cassa).<br />Exemple : <code>title matches \"football\"</code>'
                 notmatches: 'Teste se <i>subjècte</i> correspond pas a una <i>recèrca</i> (sensibla a la cassa).<br />Example : <code>title notmatches "football"</code>'
+    otp:
+        # page_title: Two-factor authentication
+        # app:
+        #     two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+        #     two_factor_code_description_2: 'You can scan that QR Code with your app:'
+        #     two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+        #     two_factor_code_description_4: 'Test an OTP code from your configured app:'
+        #     cancel: Cancel
+        #     enable: Enable
 
 entry:
     default_title: "Títol de l'article"
@@ -532,7 +549,8 @@ user:
         email_label: 'Adreça de corrièl'
         enabled_label: 'Actiu'
         last_login_label: 'Darrièra connexion'
-        twofactor_label: 'Autentificacion doble-factor'
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         save: 'Enregistrar'
         delete: 'Suprimir'
         delete_confirm: 'Sètz segur ?'
index 684c40e2839b0e61b0842f642ec26bc1e4ef1295..a7a4d6c39243a261120c0ea07a977fa70acca387 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'Hasło'
         rules: 'Zasady tagowania'
         new_user: 'Dodaj użytkownika'
+        reset: 'Reset'
     form:
         save: 'Zapisz'
     form_settings:
@@ -98,11 +99,18 @@ config:
             all: 'Wszystkie'
         rss_limit: 'Link do RSS'
     form_user:
-        two_factor_description: "Włączenie autoryzacji dwuetapowej oznacza, że będziesz otrzymywał maile z kodem przy każdym nowym, niezaufanym połączeniu"
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'Nazwa'
         email_label: 'Adres email'
-        twoFactorAuthentication_label: 'Autoryzacja dwuetapowa'
-        help_twoFactorAuthentication: "Jeżeli włączysz autoryzację dwuetapową. Za każdym razem, kiedy będziesz chciał się zalogować, dostaniesz kod na swój e-mail."
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             title: Usuń moje konto (niebezpieczna strefa !)
             description: Jeżeli usuniesz swoje konto, wszystkie twoje artykuły, tagi, adnotacje, oraz konto zostaną trwale usunięte (operacja jest NIEODWRACALNA). Następnie zostaniesz wylogowany.
@@ -160,6 +168,15 @@ config:
                 and: 'Jedna reguła I inna'
                 matches: 'Sprawdź czy <i>temat</i> pasuje <i>szukaj</i> (duże lub małe litery).<br />Przykład: <code>tytuł zawiera "piłka nożna"</code>'
                 notmatches: 'Sprawdź czy <i>temat</i> nie zawiera <i>szukaj</i> (duże lub małe litery).<br />Przykład: <code>tytuł nie zawiera "piłka nożna"</code>'
+    otp:
+        # page_title: Two-factor authentication
+        # app:
+        #     two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+        #     two_factor_code_description_2: 'You can scan that QR Code with your app:'
+        #     two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+        #     two_factor_code_description_4: 'Test an OTP code from your configured app:'
+        #     cancel: Cancel
+        #     enable: Enable
 
 entry:
     default_title: 'Tytuł wpisu'
@@ -532,7 +549,8 @@ user:
         email_label: 'Adres email'
         enabled_label: 'Włączony'
         last_login_label: 'Ostatnie logowanie'
-        twofactor_label: Autoryzacja dwuetapowa
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         save: Zapisz
         delete: Usuń
         delete_confirm: Jesteś pewien?
index 7932d7ab04923a2ab533187bad6d5674f387265a..a5483a6d3fbde2cb61e9d26927cbc6f707a22403 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'Senha'
         rules: 'Regras de tags'
         new_user: 'Adicionar um usuário'
+        # reset: 'Reset area'
     form:
         save: 'Salvar'
     form_settings:
@@ -98,11 +99,18 @@ config:
             # all: 'All'
         rss_limit: 'Número de itens no feed'
     form_user:
-        two_factor_description: 'Habilitar autenticação de dois passos significa que você receberá um e-mail com um código a cada nova conexão desconhecida.'
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'Nome'
         email_label: 'E-mail'
-        twoFactorAuthentication_label: 'Autenticação de dois passos'
-        # help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by email."
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             # title: Delete my account (a.k.a danger zone)
             # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out.
@@ -160,6 +168,15 @@ config:
                 and: 'Uma regra E outra'
                 matches: 'Testa que um <i>assunto</i> corresponde a uma <i>pesquisa</i> (maiúscula ou minúscula).<br />Exemplo: <code>título corresponde a "futebol"</code>'
                 # notmatches: 'Tests that a <i>subject</i> doesn''t match match a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
+    otp:
+        # page_title: Two-factor authentication
+        # app:
+        #     two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+        #     two_factor_code_description_2: 'You can scan that QR Code with your app:'
+        #     two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+        #     two_factor_code_description_4: 'Test an OTP code from your configured app:'
+        #     cancel: Cancel
+        #     enable: Enable
 
 entry:
     default_title: 'Título da entrada'
@@ -532,7 +549,8 @@ user:
         email_label: 'E-mail'
         enabled_label: 'Habilitado'
         last_login_label: 'Último login'
-        twofactor_label: 'Autenticação de dois passos'
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         save: 'Salvar'
         delete: 'Apagar'
         delete_confirm: 'Tem certeza?'
index 4d091f03151479b65a254470d236cb7e88a6206a..3b7fbd6917e134e44dad8db2790402fe308215a5 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'Parolă'
         # rules: 'Tagging rules'
         new_user: 'Crează un utilizator'
+        # reset: 'Reset area'
     form:
         save: 'Salvează'
     form_settings:
@@ -98,11 +99,18 @@ config:
             # all: 'All'
         rss_limit: 'Limită RSS'
     form_user:
-        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code on every new untrusted connexion"
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'Nume'
         email_label: 'E-mail'
-        # twoFactorAuthentication_label: 'Two factor authentication'
-        # help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by email."
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             # title: Delete my account (a.k.a danger zone)
             # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out.
@@ -160,6 +168,15 @@ config:
         #         and: 'One rule AND another'
         #         matches: 'Tests that a <i>subject</i> matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>'
         #         notmatches: 'Tests that a <i>subject</i> doesn''t match match a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
+    otp:
+        # page_title: Two-factor authentication
+        # app:
+        #     two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+        #     two_factor_code_description_2: 'You can scan that QR Code with your app:'
+        #     two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+        #     two_factor_code_description_4: 'Test an OTP code from your configured app:'
+        #     cancel: Cancel
+        #     enable: Enable
 
 entry:
     # default_title: 'Title of the entry'
@@ -532,7 +549,8 @@ user:
         email_label: 'E-mail'
         # enabled_label: 'Enabled'
         # last_login_label: 'Last login'
-        # twofactor_label: Two factor authentication
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         # save: Save
         # delete: Delete
         # delete_confirm: Are you sure?
index cc327ae4863ad3c349952a2cb9e4b92cb76c8007..9274663140464d222ab1a4b11c0d9f2c4da4a582 100644 (file)
@@ -58,6 +58,7 @@ config:
         password: 'Пароль'
         rules: 'Правила настройки простановки тегов'
         new_user: 'Добавить пользователя'
+        reset: 'Сброс данных'
     form:
         save: 'Сохранить'
     form_settings:
@@ -95,11 +96,18 @@ config:
             archive: 'архивные'
         rss_limit: 'Количество записей в фиде'
     form_user:
-        two_factor_description: "Включить двухфакторную аутентификацию, Вы получите сообщение на указанный email с кодом, при каждом новом непроверенном подключении."
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'Имя'
         email_label: 'Email'
-        twoFactorAuthentication_label: 'Двухфакторная аутентификация'
-        help_twoFactorAuthentication: "Если Вы включите двухфакторную аутентификацию, то Вы будете получать код на указанный ранее email, каждый раз при входе в wallabag."
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             title: "Удалить мой аккаунт (или опасная зона)"
             description: "Если Вы удалите ваш аккаунт, ВСЕ ваши записи, теги и другие данные, будут БЕЗВОЗВРАТНО удалены (операция не может быть отменена после). Затем Вы выйдете из системы."
@@ -155,6 +163,15 @@ config:
                 or: 'Одно правило ИЛИ другое'
                 and: 'Одно правило И другое'
                 matches: 'Тесты, в которых <i> тема </i> соответствует <i> поиску </i> (без учета регистра). Пример: <code> title matches "футбол" </code>'
+    otp:
+        # page_title: Two-factor authentication
+        # app:
+        #     two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+        #     two_factor_code_description_2: 'You can scan that QR Code with your app:'
+        #     two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+        #     two_factor_code_description_4: 'Test an OTP code from your configured app:'
+        #     cancel: Cancel
+        #     enable: Enable
 
 entry:
     default_title: 'Название записи'
@@ -520,7 +537,8 @@ user:
         email_label: 'Email'
         enabled_label: 'Включить'
         last_login_label: 'Последний вход'
-        twofactor_label: "Двухфакторная аутентификация"
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         save: "Сохранить"
         delete: "Удалить"
         delete_confirm: "Вы уверены?"
index 148aa541159aef4cefcf015582627b450d21054a..1fe4fa0ea51ae045cc7eda1460d86adc8a92cddd 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'รหัสผ่าน'
         rules: 'การแท็กข้อบังคับ'
         new_user: 'เพิ่มผู้ใช้'
+        reset: 'รีเซ็ตพื้นที่ '
     form:
         save: 'บันทึก'
     form_settings:
@@ -98,11 +99,18 @@ config:
             all: 'ทั้งหมด'
         rss_limit: 'จำนวนไอเทมที่เก็บ'
     form_user:
-        two_factor_description: "การเปิดใช้งาน two factor authentication คือคุณจะต้องได้รับอีเมลกับ code ที่ยังไม่ตรวจสอบในการเชื่อมต่อ"
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'ชื่อ'
         email_label: 'อีเมล'
-        twoFactorAuthentication_label: 'Two factor authentication'
-        help_twoFactorAuthentication: "ถ้าคุณเปิด 2FA, ในแต่ละช่วงเวลาที่คุณต้องการลงชื่อเข้าใช wallabag, คุณจะต้องได้รับ code จากอีเมล"
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             title: ลบบัญชีของฉัน (โซนที่เป็นภัย!)
             description: ถ้าคุณลบบัญชีของคุณIf , รายการทั้งหมดของคุณ, แท็กทั้งหมดของคุณ, หมายเหตุทั้งหมดของคุณและบัญชีของคุณจะถูกลบอย่างถาวร (มันไม่สามารถยกเลิกได้) คุณจะต้องลงชื่อออก
@@ -160,6 +168,15 @@ config:
                 and: 'หนึ่งข้อบังคับและอื่นๆ'
                 matches: 'ทดสอบว่า <i>เรื่อง</i> นี้ตรงกับ <i>การต้นหา</i> (กรณีไม่ทราบ).<br />ตัวอย่าง: <code>หัวข้อที่ตรงกับ "football"</code>'
                 notmatches: 'ทดสอบว่า <i>เรื่อง</i> นี้ไม่ตรงกับ <i>การต้นหา</i> (กรณีไม่ทราบ).<br />ตัวอย่าง: <code>หัวข้อทีไม่ตรงกับ "football"</code>'
+    otp:
+        # page_title: Two-factor authentication
+        # app:
+        #     two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+        #     two_factor_code_description_2: 'You can scan that QR Code with your app:'
+        #     two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+        #     two_factor_code_description_4: 'Test an OTP code from your configured app:'
+        #     cancel: Cancel
+        #     enable: Enable
 
 entry:
     default_title: 'หัวข้อรายการ'
@@ -530,7 +547,8 @@ user:
         email_label: 'อีเมล'
         enabled_label: 'เปิดใช้งาน'
         last_login_label: 'ลงชื้อเข้าใช้ครั้งสุดท้าย'
-        twofactor_label: Two factor authentication
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         save: บันทึก
         delete: ลบ
         delete_confirm: ตุณแน่ใจหรือไม่?
index 6fb9852a30968a7b7774db01d6751fde5f842c38..3b8a0d599948ece20605550b2f7f5519d48efcf2 100644 (file)
@@ -59,6 +59,7 @@ config:
         password: 'Şifre'
         rules: 'Etiketleme kuralları'
         new_user: 'Bir kullanıcı ekle'
+        # reset: 'Reset area'
     form:
         save: 'Kaydet'
     form_settings:
@@ -98,11 +99,18 @@ config:
             # all: 'All'
         rss_limit: 'RSS içeriğinden talep edilecek makale limiti'
     form_user:
-        two_factor_description: "İki adımlı doğrulamayı aktifleştirdiğinizde, her yeni güvenilmeyen bağlantılarda size e-posta ile bir kod alacaksınız."
+        # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option."
         name_label: 'İsim'
         email_label: 'E-posta'
-        twoFactorAuthentication_label: 'İki adımlı doğrulama'
-        # help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by email."
+            # emailTwoFactor_label: 'Using email (receive a code by email)'
+            # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)'
+            # table_method: Method
+            # table_state: State
+            # table_action: Action
+            # state_enabled: Enabled
+            # state_disabled: Disabled
+            # action_email: Use email
+            # action_app: Use OTP App
         delete:
             # title: Delete my account (a.k.a danger zone)
             # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out.
@@ -160,6 +168,15 @@ config:
                 and: 'Bir kural ve diğeri'
                 # matches: 'Tests that a <i>subject</i> matches a <i>search</i> (case-insensitive).<br />Example: <code>title matches "football"</code>'
                 # notmatches: 'Tests that a <i>subject</i> doesn''t match match a <i>search</i> (case-insensitive).<br />Example: <code>title notmatches "football"</code>'
+    otp:
+        # page_title: Two-factor authentication
+        # app:
+        #     two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload.
+        #     two_factor_code_description_2: 'You can scan that QR Code with your app:'
+        #     two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:'
+        #     two_factor_code_description_4: 'Test an OTP code from your configured app:'
+        #     cancel: Cancel
+        #     enable: Enable
 
 entry:
     default_title: 'Makalenin başlığı'
@@ -530,7 +547,8 @@ user:
         email_label: 'E-posta'
         # enabled_label: 'Enabled'
         # last_login_label: 'Last login'
-        # twofactor_label: Two factor authentication
+        # twofactor_email_label: Two factor authentication by email
+        # twofactor_google_label: Two factor authentication by OTP app
         # save: Save
         # delete: Delete
         # delete_confirm: Are you sure?
index bcc57dace74b9c78a108315406f2e52476ef1a7b..93f8ddf8ace6fd319e8031e13389e92283a5c883 100644 (file)
@@ -86,8 +86,7 @@
                 <br/>
                 <img id="androidQrcode" />
                 <script>
-                    const imgBase64 = jrQrcode.getQrBase64('wallabag://{{ app.user.username }}@{{ wallabag_url }}');
-                    document.getElementById('androidQrcode').src = imgBase64;
+                    document.getElementById('androidQrcode').src = jrQrcode.getQrBase64('wallabag://{{ app.user.username }}@{{ wallabag_url }}');
                 </script>
             </div>
         </fieldset>
             </div>
         </fieldset>
 
+        {{ form_widget(form.user.save) }}
+
         {% if twofactor_auth %}
+        <h5>{{ 'config.otp.page_title'|trans }}</h5>
+
         <div class="row">
             {{ 'config.form_user.two_factor_description'|trans }}
         </div>
 
-        <fieldset class="w500p inline">
-            <div class="row">
-                {{ form_label(form.user.twoFactorAuthentication) }}
-                {{ form_errors(form.user.twoFactorAuthentication) }}
-                {{ form_widget(form.user.twoFactorAuthentication) }}
-            </div>
-            <a href="#" title="{{ 'config.form_user.help_twoFactorAuthentication'|trans }}">
-                <i class="material-icons">live_help</i>
-            </a>
-        </fieldset>
-        {% endif %}
+        <table>
+            <thead>
+                <tr>
+                    <th>{{ 'config.form_user.two_factor.table_method'|trans }}</th>
+                    <th>{{ 'config.form_user.two_factor.table_state'|trans }}</th>
+                    <th>{{ 'config.form_user.two_factor.table_action'|trans }}</th>
+                </tr>
+            </thead>
 
-        <h2>{{ 'config.reset.title'|trans }}</h2>
-        <fieldset class="w500p inline">
-            <p>{{ 'config.reset.description'|trans }}</p>
-            <ul>
-                <li>
-                    <a href="{{ path('config_reset', { type: 'annotations'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
-                        {{ 'config.reset.annotations'|trans }}
-                    </a>
-                </li>
-                <li>
-                    <a href="{{ path('config_reset', { type: 'tags'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
-                        {{ 'config.reset.tags'|trans }}
-                    </a>
-                </li>
-                <li>
-                    <a href="{{ path('config_reset', { type: 'archived'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
-                        {{ 'config.reset.archived'|trans }}
-                    </a>
-                </li>
-                <li>
-                    <a href="{{ path('config_reset', { type: 'entries'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
-                        {{ 'config.reset.entries'|trans }}
-                    </a>
-                </li>
-            </ul>
-        </fieldset>
+            <tbody>
+                <tr>
+                    <td>{{ 'config.form_user.two_factor.emailTwoFactor_label'|trans }}</td>
+                    <td>{% if app.user.isEmailTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
+                    <td><a href="{{ path('config_otp_email') }}" class="waves-effect waves-light btn{% if app.user.isEmailTwoFactor %} disabled{% endif %}">{{ 'config.form_user.two_factor.action_email'|trans }}</a></td>
+                </tr>
+                <tr>
+                    <td>{{ 'config.form_user.two_factor.googleTwoFactor_label'|trans }}</td>
+                    <td>{% if app.user.isGoogleTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
+                    <td><a href="{{ path('config_otp_app') }}" class="waves-effect waves-light btn{% if app.user.isGoogleTwoFactor %} disabled{% endif %}">{{ 'config.form_user.two_factor.action_app'|trans }}</a></td>
+                </tr>
+            </tbody>
+        </table>
+
+        {% endif %}
 
         {{ form_widget(form.user._token) }}
-        {{ form_widget(form.user.save) }}
     </form>
 
     {% if enabled_users > 1 %}
         {% endfor %}
     </ul>
 
-        {{ form_start(form.new_tagging_rule) }}
+    {{ form_start(form.new_tagging_rule) }}
         {{ form_errors(form.new_tagging_rule) }}
 
         <fieldset class="w500p inline">
             </table>
         </div>
     </div>
+
+    <h2>{{ 'config.reset.title'|trans }}</h2>
+    <fieldset class="w500p inline">
+        <p>{{ 'config.reset.description'|trans }}</p>
+        <ul>
+            <li>
+                <a href="{{ path('config_reset', { type: 'annotations'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                    {{ 'config.reset.annotations'|trans }}
+                </a>
+            </li>
+            <li>
+                <a href="{{ path('config_reset', { type: 'tags'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                    {{ 'config.reset.tags'|trans }}
+                </a>
+            </li>
+            <li>
+                <a href="{{ path('config_reset', { type: 'archived'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                    {{ 'config.reset.archived'|trans }}
+                </a>
+            </li>
+            <li>
+                <a href="{{ path('config_reset', { type: 'entries'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                    {{ 'config.reset.entries'|trans }}
+                </a>
+            </li>
+        </ul>
+    </fieldset>
 {% endblock %}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/otp_app.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/otp_app.html.twig
new file mode 100644 (file)
index 0000000..0919646
--- /dev/null
@@ -0,0 +1,55 @@
+{% extends "WallabagCoreBundle::layout.html.twig" %}
+
+{% block title %}{{ 'config.page_title'|trans }} > {{ 'config.otp.page_title'|trans }}{% endblock %}
+
+{% block content %}
+    <h5>{{ 'config.otp.page_title'|trans }}</h5>
+
+    <ol>
+        <li>
+            <p>{{ 'config.otp.app.two_factor_code_description_1'|trans }}</p>
+            <p>{{ 'config.otp.app.two_factor_code_description_2'|trans }}</p>
+
+            <p>
+                <img id="2faQrcode" class="hide-on-med-and-down" />
+                <script>
+                    document.getElementById('2faQrcode').src = jrQrcode.getQrBase64('{{ qr_code }}');
+                </script>
+            </p>
+        </li>
+        <li>
+            <p>{{ 'config.otp.app.two_factor_code_description_3'|trans }}</p>
+
+            <p><strong>{{ backupCodes|join("\n")|nl2br }}</strong></p>
+        </li>
+        <li>
+            <p>{{ 'config.otp.app.two_factor_code_description_4'|trans }}</p>
+
+            {% for flashMessage in app.session.flashbag.get("two_factor") %}
+            <div class="card-panel red darken-1 black-text">
+                {{ flashMessage|trans }}
+            </div>
+            {% endfor %}
+
+            <form class="form" action="{{ path("config_otp_app_check") }}" method="post">
+                <div class="card-content">
+                    <div class="row">
+                        <div class="input-field col s12">
+                            <label for="_auth_code">{{ "scheb_two_factor.auth_code"|trans }}</label>
+                            <input id="_auth_code" type="text" autocomplete="off" name="_auth_code" />
+                        </div>
+                    </div>
+                </div>
+                <div class="card-action">
+                    <a href="{{ path('config_otp_app_cancel') }}" class="waves-effect waves-light grey btn">
+                        {{ 'config.otp.app.cancel'|trans }}
+                    </a>
+                    <button class="btn waves-effect waves-light" type="submit" name="send">
+                        {{ 'config.otp.app.enable'|trans }}
+                        <i class="material-icons right">send</i>
+                    </button>
+                </div>
+            </form>
+        </li>
+    </ol>
+{% endblock %}
index f896fe2d5304d25079e373499eaf706e38f5f61b..412c18f49f3bdac46dba42d4fe52df54aa8ec2e6 100644 (file)
@@ -16,6 +16,7 @@
                             <li class="tab col s12 m6 l3"><a href="#set3">{{ 'config.tab_menu.user_info'|trans }}</a></li>
                             <li class="tab col s12 m6 l3"><a href="#set4">{{ 'config.tab_menu.password'|trans }}</a></li>
                             <li class="tab col s12 m6 l3"><a href="#set5">{{ 'config.tab_menu.rules'|trans }}</a></li>
+                            <li class="tab col s12 m6 l3"><a href="#set6">{{ 'config.tab_menu.reset'|trans }}</a></li>
                         </ul>
                     </div>
 
                                     <img id="androidQrcode" class="hide-on-med-and-down" />
                                 </div>
                                 <script>
-                                    const imgBase64 = jrQrcode.getQrBase64('wallabag://{{ app.user.username }}@{{ wallabag_url }}');
-                                    document.getElementById('androidQrcode').src = imgBase64;
+                                    document.getElementById('androidQrcode').src = jrQrcode.getQrBase64('wallabag://{{ app.user.username }}@{{ wallabag_url }}');
                                 </script>
                             </div>
 
                                 </div>
                             </div>
 
-                            {% if twofactor_auth %}
-                            <div class="row">
-                                <div class="input-field col s11">
-                                    {{ 'config.form_user.two_factor_description'|trans }}
-
-                                    <br />
+                            {{ form_widget(form.user.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
 
-                                    {{ form_widget(form.user.twoFactorAuthentication) }}
-                                    {{ form_label(form.user.twoFactorAuthentication) }}
-                                    {{ form_errors(form.user.twoFactorAuthentication) }}
-                                </div>
-                                <div class="input-field col s1">
-                                    <a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_user.help_twoFactorAuthentication'|trans }}">
-                                        <i class="material-icons">live_help</i>
-                                    </a>
+                            {% if twofactor_auth %}
+                                <br/>
+                                <br/>
+                                <div class="row">
+                                    <h5>{{ 'config.otp.page_title'|trans }}</h5>
+
+                                    <p>{{ 'config.form_user.two_factor_description'|trans }}</p>
+
+                                    <table>
+                                        <thead>
+                                            <tr>
+                                                <th>{{ 'config.form_user.two_factor.table_method'|trans }}</th>
+                                                <th>{{ 'config.form_user.two_factor.table_state'|trans }}</th>
+                                                <th>{{ 'config.form_user.two_factor.table_action'|trans }}</th>
+                                            </tr>
+                                        </thead>
+
+                                        <tbody>
+                                            <tr>
+                                                <td>{{ 'config.form_user.two_factor.emailTwoFactor_label'|trans }}</td>
+                                                <td>{% if app.user.isEmailTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
+                                                <td><a href="{{ path('config_otp_email') }}" class="waves-effect waves-light btn{% if app.user.isEmailTwoFactor %} disabled{% endif %}">{{ 'config.form_user.two_factor.action_email'|trans }}</a></td>
+                                            </tr>
+                                            <tr>
+                                                <td>{{ 'config.form_user.two_factor.googleTwoFactor_label'|trans }}</td>
+                                                <td>{% if app.user.isGoogleTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
+                                                <td><a href="{{ path('config_otp_app') }}" class="waves-effect waves-light btn{% if app.user.isGoogleTwoFactor %} disabled{% endif %}">{{ 'config.form_user.two_factor.action_app'|trans }}</a></td>
+                                            </tr>
+                                        </tbody>
+                                    </table>
                                 </div>
-                            </div>
                             {% endif %}
-
-                            {{ form_widget(form.user.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
                             {{ form_widget(form.user._token) }}
                         </form>
-
-                        <br /><hr /><br />
-
-                        <div class="row">
-                            <h5>{{ 'config.reset.title'|trans }}</h5>
-                            <p>{{ 'config.reset.description'|trans }}</p>
-                            <a href="{{ path('config_reset', { type: 'annotations'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
-                                {{ 'config.reset.annotations'|trans }}
-                            </a>
-                            <a href="{{ path('config_reset', { type: 'tags'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
-                                {{ 'config.reset.tags'|trans }}
-                            </a>
-                            <a href="{{ path('config_reset', { type: 'archived'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
-                                {{ 'config.reset.archived'|trans }}
-                            </a>
-                            <a href="{{ path('config_reset', { type: 'entries'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
-                                {{ 'config.reset.entries'|trans }}
-                            </a>
-                        </div>
-
-                        {% if enabled_users > 1 %}
-                            <br /><hr /><br />
-
-                            <div class="row">
-                                <h5>{{ 'config.form_user.delete.title'|trans }}</h5>
-                                <p>{{ 'config.form_user.delete.description'|trans }}</p>
-                                <a href="{{ path('delete_account') }}" onclick="return confirm('{{ 'config.form_user.delete.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red delete-account">
-                                    {{ 'config.form_user.delete.button'|trans }}
-                                </a>
-                            </div>
-                        {% endif %}
                     </div>
 
                     <div id="set4" class="col s12">
                             </div>
                         </div>
                     </div>
+
+                    <div id="set6" class="col s12">
+                        <div class="row">
+                            <h5>{{ 'config.reset.title'|trans }}</h5>
+                            <p>{{ 'config.reset.description'|trans }}</p>
+                            <a href="{{ path('config_reset', { type: 'annotations'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                                {{ 'config.reset.annotations'|trans }}
+                            </a>
+                            <a href="{{ path('config_reset', { type: 'tags'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                                {{ 'config.reset.tags'|trans }}
+                            </a>
+                            <a href="{{ path('config_reset', { type: 'archived'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                                {{ 'config.reset.archived'|trans }}
+                            </a>
+                            <a href="{{ path('config_reset', { type: 'entries'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                                {{ 'config.reset.entries'|trans }}
+                            </a>
+                        </div>
+
+                        {% if enabled_users > 1 %}
+                            <br /><hr /><br />
+
+                            <div class="row">
+                                <h5>{{ 'config.form_user.delete.title'|trans }}</h5>
+                                <p>{{ 'config.form_user.delete.description'|trans }}</p>
+                                <a href="{{ path('delete_account') }}" onclick="return confirm('{{ 'config.form_user.delete.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red delete-account">
+                                    {{ 'config.form_user.delete.button'|trans }}
+                                </a>
+                            </div>
+                        {% endif %}
+                    </div>
                 </div>
 
             </div>
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/otp_app.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/otp_app.html.twig
new file mode 100644 (file)
index 0000000..7875d78
--- /dev/null
@@ -0,0 +1,63 @@
+{% extends "WallabagCoreBundle::layout.html.twig" %}
+
+{% block title %}{{ 'config.page_title'|trans }} > {{ 'config.otp.page_title'|trans }}{% endblock %}
+
+{% block content %}
+    <div class="row">
+        <div class="col s12">
+            <div class="card-panel settings">
+                <div class="row">
+                    <h5>{{ 'config.otp.page_title'|trans }}</h5>
+
+                    <ol>
+                        <li>
+                            <p>{{ 'config.otp.app.two_factor_code_description_1'|trans }}</p>
+                            <p>{{ 'config.otp.app.two_factor_code_description_2'|trans }}</p>
+
+                            <p>
+                                <img id="2faQrcode" class="hide-on-med-and-down" />
+                                <script>
+                                    document.getElementById('2faQrcode').src = jrQrcode.getQrBase64('{{ qr_code }}');
+                                </script>
+                            </p>
+                        </li>
+                        <li>
+                            <p>{{ 'config.otp.app.two_factor_code_description_3'|trans }}</p>
+
+                            <p><strong>{{ backupCodes|join("\n")|nl2br }}</strong></p>
+                        </li>
+                        <li>
+                            <p>{{ 'config.otp.app.two_factor_code_description_4'|trans }}</p>
+
+                            {% for flashMessage in app.session.flashbag.get("two_factor") %}
+                            <div class="card-panel red darken-1 black-text">
+                                {{ flashMessage|trans }}
+                            </div>
+                            {% endfor %}
+
+                            <form class="form" action="{{ path("config_otp_app_check") }}" method="post">
+                                <div class="card-content">
+                                    <div class="row">
+                                        <div class="input-field col s12">
+                                            <label for="_auth_code">{{ "scheb_two_factor.auth_code"|trans }}</label>
+                                            <input id="_auth_code" type="text" autocomplete="off" name="_auth_code" />
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="card-action">
+                                    <a href="{{ path('config_otp_app_cancel') }}" class="waves-effect waves-light grey btn">
+                                        {{ 'config.otp.app.cancel'|trans }}
+                                    </a>
+                                    <button class="btn waves-effect waves-light" type="submit" name="send">
+                                        {{ 'config.otp.app.enable'|trans }}
+                                        <i class="material-icons right">send</i>
+                                    </button>
+                                </div>
+                            </form>
+                        </li>
+                    </ol>
+                </div>
+            </div>
+        </div>
+    </div>
+{% endblock %}
index a9746fb47d1ecc5c431e9b58862081c5bbe44a92..63a06206150d6cba59766a6adeaec345847e4f1a 100644 (file)
@@ -62,14 +62,29 @@ class ManageController extends Controller
      */
     public function editAction(Request $request, User $user)
     {
+        $userManager = $this->container->get('fos_user.user_manager');
+
         $deleteForm = $this->createDeleteForm($user);
-        $editForm = $this->createForm('Wallabag\UserBundle\Form\UserType', $user);
-        $editForm->handleRequest($request);
+        $form = $this->createForm('Wallabag\UserBundle\Form\UserType', $user);
+        $form->handleRequest($request);
 
-        if ($editForm->isSubmitted() && $editForm->isValid()) {
-            $em = $this->getDoctrine()->getManager();
-            $em->persist($user);
-            $em->flush();
+        // `googleTwoFactor` isn't a field within the User entity, we need to define it's value in a different way
+        if ($this->getParameter('twofactor_auth') && true === $user->isGoogleAuthenticatorEnabled() && false === $form->isSubmitted()) {
+            $form->get('googleTwoFactor')->setData(true);
+        }
+
+        if ($form->isSubmitted() && $form->isValid()) {
+            // handle creation / reset of the OTP secret if checkbox changed from the previous state
+            if ($this->getParameter('twofactor_auth')) {
+                if (true === $form->get('googleTwoFactor')->getData() && false === $user->isGoogleAuthenticatorEnabled()) {
+                    $user->setGoogleAuthenticatorSecret($this->get('scheb_two_factor.security.google_authenticator')->generateSecret());
+                    $user->setEmailTwoFactor(false);
+                } elseif (false === $form->get('googleTwoFactor')->getData() && true === $user->isGoogleAuthenticatorEnabled()) {
+                    $user->setGoogleAuthenticatorSecret(null);
+                }
+            }
+
+            $userManager->updateUser($user);
 
             $this->get('session')->getFlashBag()->add(
                 'notice',
@@ -81,7 +96,7 @@ class ManageController extends Controller
 
         return $this->render('WallabagUserBundle:Manage:edit.html.twig', [
             'user' => $user,
-            'edit_form' => $editForm->createView(),
+            'edit_form' => $form->createView(),
             'delete_form' => $deleteForm->createView(),
             'twofactor_auth' => $this->getParameter('twofactor_auth'),
         ]);
@@ -131,8 +146,6 @@ class ManageController extends Controller
         $form->handleRequest($request);
 
         if ($form->isSubmitted() && $form->isValid()) {
-            $this->get('logger')->info('searching users');
-
             $searchTerm = (isset($request->get('search_user')['term']) ? $request->get('search_user')['term'] : '');
 
             $qb = $em->getRepository('WallabagUserBundle:User')->getQueryBuilderForSearch($searchTerm);
@@ -157,7 +170,7 @@ class ManageController extends Controller
     }
 
     /**
-     * Creates a form to delete a User entity.
+     * Create a form to delete a User entity.
      *
      * @param User $user The User entity
      *
index 48446e3c1a6e64be30725ac03b77e2a636313fd4..43fa6a80fc2bb47b40970b936a21a2612de0a59a 100644 (file)
@@ -8,8 +8,9 @@ use FOS\UserBundle\Model\User as BaseUser;
 use JMS\Serializer\Annotation\Accessor;
 use JMS\Serializer\Annotation\Groups;
 use JMS\Serializer\Annotation\XmlRoot;
-use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
-use Scheb\TwoFactorBundle\Model\TrustedComputerInterface;
+use Scheb\TwoFactorBundle\Model\BackupCodeInterface;
+use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface as EmailTwoFactorInterface;
+use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface as GoogleTwoFactorInterface;
 use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
 use Symfony\Component\Security\Core\User\UserInterface;
 use Wallabag\ApiBundle\Entity\Client;
@@ -28,7 +29,7 @@ use Wallabag\CoreBundle\Helper\EntityTimestampsTrait;
  * @UniqueEntity("email")
  * @UniqueEntity("username")
  */
-class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterface
+class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorInterface, BackupCodeInterface
 {
     use EntityTimestampsTrait;
 
@@ -123,16 +124,21 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
     private $authCode;
 
     /**
-     * @var bool
-     *
-     * @ORM\Column(type="boolean")
+     * @ORM\Column(name="googleAuthenticatorSecret", type="string", nullable=true)
      */
-    private $twoFactorAuthentication = false;
+    private $googleAuthenticatorSecret;
 
     /**
      * @ORM\Column(type="json_array", nullable=true)
      */
-    private $trusted;
+    private $backupCodes;
+
+    /**
+     * @var bool
+     *
+     * @ORM\Column(type="boolean")
+     */
+    private $emailTwoFactor = false;
 
     public function __construct()
     {
@@ -233,49 +239,119 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
     /**
      * @return bool
      */
-    public function isTwoFactorAuthentication()
+    public function isEmailTwoFactor()
     {
-        return $this->twoFactorAuthentication;
+        return $this->emailTwoFactor;
     }
 
     /**
-     * @param bool $twoFactorAuthentication
+     * @param bool $emailTwoFactor
      */
-    public function setTwoFactorAuthentication($twoFactorAuthentication)
+    public function setEmailTwoFactor($emailTwoFactor)
     {
-        $this->twoFactorAuthentication = $twoFactorAuthentication;
+        $this->emailTwoFactor = $emailTwoFactor;
     }
 
-    public function isEmailAuthEnabled()
+    /**
+     * Used in the user config form to be "like" the email option.
+     */
+    public function isGoogleTwoFactor()
+    {
+        return $this->isGoogleAuthenticatorEnabled();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isEmailAuthEnabled(): bool
     {
-        return $this->twoFactorAuthentication;
+        return $this->emailTwoFactor;
     }
 
-    public function getEmailAuthCode()
+    /**
+     * {@inheritdoc}
+     */
+    public function getEmailAuthCode(): string
     {
         return $this->authCode;
     }
 
-    public function setEmailAuthCode($authCode)
+    /**
+     * {@inheritdoc}
+     */
+    public function setEmailAuthCode(string $authCode): void
     {
         $this->authCode = $authCode;
     }
 
-    public function addTrustedComputer($token, \DateTime $validUntil)
+    /**
+     * {@inheritdoc}
+     */
+    public function getEmailAuthRecipient(): string
     {
-        $this->trusted[$token] = $validUntil->format('r');
+        return $this->email;
     }
 
-    public function isTrustedComputer($token)
+    /**
+     * {@inheritdoc}
+     */
+    public function isGoogleAuthenticatorEnabled(): bool
     {
-        if (isset($this->trusted[$token])) {
-            $now = new \DateTime();
-            $validUntil = new \DateTime($this->trusted[$token]);
+        return $this->googleAuthenticatorSecret ? true : false;
+    }
 
-            return $now < $validUntil;
-        }
+    /**
+     * {@inheritdoc}
+     */
+    public function getGoogleAuthenticatorUsername(): string
+    {
+        return $this->username;
+    }
 
-        return false;
+    /**
+     * {@inheritdoc}
+     */
+    public function getGoogleAuthenticatorSecret(): string
+    {
+        return $this->googleAuthenticatorSecret;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): void
+    {
+        $this->googleAuthenticatorSecret = $googleAuthenticatorSecret;
+    }
+
+    public function setBackupCodes(array $codes = null)
+    {
+        $this->backupCodes = $codes;
+    }
+
+    public function getBackupCodes()
+    {
+        return $this->backupCodes;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isBackupCode(string $code): bool
+    {
+        return false === $this->findBackupCode($code) ? false : true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function invalidateBackupCode(string $code): void
+    {
+        $key = $this->findBackupCode($code);
+
+        if (false !== $key) {
+            unset($this->backupCodes[$key]);
+        }
     }
 
     /**
@@ -309,4 +385,24 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
             return $this->clients->first();
         }
     }
+
+    /**
+     * Try to find a backup code from the list of backup codes of the current user.
+     *
+     * @param string $code Given code from the user
+     *
+     * @return string|false
+     */
+    private function findBackupCode(string $code)
+    {
+        foreach ($this->backupCodes as $key => $backupCode) {
+            // backup code are hashed using `password_hash`
+            // see ConfigController->otpAppAction
+            if (password_verify($code, $backupCode)) {
+                return $key;
+            }
+        }
+
+        return false;
+    }
 }
index 56fea640ba359283baa8db43c719a89ee770f0a4..026db9a2c23bf65988ae21ae6bcb2c3f436ba347 100644 (file)
@@ -35,9 +35,14 @@ class UserType extends AbstractType
                 'required' => false,
                 'label' => 'user.form.enabled_label',
             ])
-            ->add('twoFactorAuthentication', CheckboxType::class, [
+            ->add('emailTwoFactor', CheckboxType::class, [
                 'required' => false,
-                'label' => 'user.form.twofactor_label',
+                'label' => 'user.form.twofactor_email_label',
+            ])
+            ->add('googleTwoFactor', CheckboxType::class, [
+                'required' => false,
+                'label' => 'user.form.twofactor_google_label',
+                'mapped' => false,
             ])
             ->add('save', SubmitType::class, [
                 'label' => 'user.form.save',
index aed805c957c2a37c2a4c6a954e54991d9e2afb9e..2797efde9edb3aa90c4ea4199c9f1f6a185351a7 100644 (file)
@@ -78,7 +78,7 @@ class AuthCodeMailer implements AuthCodeMailerInterface
      *
      * @param TwoFactorInterface $user
      */
-    public function sendAuthCode(TwoFactorInterface $user)
+    public function sendAuthCode(TwoFactorInterface $user): void
     {
         $template = $this->twig->loadTemplate('WallabagUserBundle:TwoFactor:email_auth_code.html.twig');
 
@@ -97,7 +97,7 @@ class AuthCodeMailer implements AuthCodeMailerInterface
 
         $message = new \Swift_Message();
         $message
-            ->setTo($user->getEmail())
+            ->setTo($user->getEmailAuthRecipient())
             ->setFrom($this->senderEmail, $this->senderName)
             ->setSubject($subject)
             ->setBody($bodyText, 'text/plain')
index c8471bdda62fa926df0b70269a3870acc01a508c..47a5cb784cfd2c1f7a6ce362ce640bf224612b87 100644 (file)
@@ -1,7 +1,8 @@
+{# Override `vendor/scheb/two-factor-bundle/Resources/views/Authentication/form.html.twig` #}
 {% extends "WallabagUserBundle::layout.html.twig" %}
 
 {% block fos_user_content %}
-<form class="form" action="" method="post">
+<form class="form" action="{{ path("2fa_login_check") }}" method="post">
     <div class="card-content">
         <div class="row">
 
             <p class="error">{{ flashMessage|trans }}</p>
             {% endfor %}
 
+            {# Authentication errors #}
+            {% if authenticationError %}
+            <p class="error">{{ authenticationError|trans(authenticationErrorData) }}</p>
+            {% endif %}
+
             <div class="input-field col s12">
                 <label for="_auth_code">{{ "scheb_two_factor.auth_code"|trans }}</label>
-                <input id="_auth_code" type="text" autocomplete="off" name="_auth_code" />
+                <input id="_auth_code" type="text" autocomplete="off" name="{{ authCodeParameterName }}" />
             </div>
 
-            {% if useTrustedOption %}
+            {% if displayTrustedOption %}
             <div class="input-field col s12">
-                <input id="_trusted" type="checkbox" name="_trusted" />
+                <input id="_trusted" type="checkbox" name="{{ trustedParameterName }}" />
                 <label for="_trusted">{{ "scheb_two_factor.trusted"|trans }}</label>
             </div>
             {% endif %}
index 3ffd15f5d4b015b14e59e32034792621fff463a7..2de8f3a55152205487d66af79764c07a00797d22 100644 (file)
                                 {% if twofactor_auth %}
                                 <div class="row">
                                     <div class="input-field col s12">
-                                        {{ form_widget(edit_form.twoFactorAuthentication) }}
-                                        {{ form_label(edit_form.twoFactorAuthentication) }}
-                                        {{ form_errors(edit_form.twoFactorAuthentication) }}
+                                        {{ form_widget(edit_form.emailTwoFactor) }}
+                                        {{ form_label(edit_form.emailTwoFactor) }}
+                                        {{ form_errors(edit_form.emailTwoFactor) }}
+                                    </div>
+                                    <div class="input-field col s12">
+                                        {{ form_widget(edit_form.googleTwoFactor) }}
+                                        {{ form_label(edit_form.googleTwoFactor) }}
+                                        {{ form_errors(edit_form.googleTwoFactor) }}
                                     </div>
                                 </div>
                                 {% endif %}
index 9b34f2a087c421ba5b631d203d0221b24fcdd44e..ed383a2c9c292065d8e732d029b9a83487c8e9c4 100644 (file)
@@ -59,7 +59,8 @@ class ShowUserCommandTest extends WallabagCoreTestCase
         $this->assertContains('Username: admin', $tester->getDisplay());
         $this->assertContains('Email: bigboss@wallabag.org', $tester->getDisplay());
         $this->assertContains('Display name: Big boss', $tester->getDisplay());
-        $this->assertContains('2FA activated: no', $tester->getDisplay());
+        $this->assertContains('2FA (email) activated', $tester->getDisplay());
+        $this->assertContains('2FA (OTP) activated', $tester->getDisplay());
     }
 
     public function testShowUser()
index c9dbbaa3b5afd0ab4a117821ae8105f4d8a59b79..1090a686bdd1ea1e2158755af568974f27e82e8f 100644 (file)
@@ -1000,4 +1000,85 @@ class ConfigControllerTest extends WallabagCoreTestCase
         $this->assertNotSame('yuyuyuyu', $client->getRequest()->getLocale());
         $this->assertNotSame('yuyuyuyu', $client->getContainer()->get('session')->get('_locale'));
     }
+
+    public function testUserEnable2faEmail()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/config/otp/email');
+
+        $this->assertSame(302, $client->getResponse()->getStatusCode());
+
+        $crawler = $client->followRedirect();
+
+        $this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text']));
+        $this->assertContains('flashes.config.notice.otp_enabled', $alert[0]);
+
+        // restore user
+        $em = $this->getEntityManager();
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUsername('admin');
+
+        $this->assertTrue($user->isEmailTwoFactor());
+
+        $user->setEmailTwoFactor(false);
+        $em->persist($user);
+        $em->flush();
+    }
+
+    public function testUserEnable2faGoogle()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/config/otp/app');
+
+        $this->assertSame(200, $client->getResponse()->getStatusCode());
+
+        // restore user
+        $em = $this->getEntityManager();
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUsername('admin');
+
+        $this->assertTrue($user->isGoogleTwoFactor());
+        $this->assertGreaterThan(0, $user->getBackupCodes());
+
+        $user->setGoogleAuthenticatorSecret(false);
+        $user->setBackupCodes(null);
+        $em->persist($user);
+        $em->flush();
+    }
+
+    public function testUserEnable2faGoogleCancel()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/config/otp/app');
+
+        $this->assertSame(200, $client->getResponse()->getStatusCode());
+
+        // restore user
+        $em = $this->getEntityManager();
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUsername('admin');
+
+        $this->assertTrue($user->isGoogleTwoFactor());
+        $this->assertGreaterThan(0, $user->getBackupCodes());
+
+        $crawler = $client->request('GET', '/config/otp/app/cancel');
+
+        $this->assertSame(302, $client->getResponse()->getStatusCode());
+
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUsername('admin');
+
+        $this->assertFalse($user->isGoogleTwoFactor());
+        $this->assertEmpty($user->getBackupCodes());
+    }
 }
index 395208a2fe263b3cfe977e03ad5271250f8ce016..b03c7550dbed8204e9b0829ba41e9f49bb3fc3de 100644 (file)
@@ -26,7 +26,7 @@ class SecurityControllerTest extends WallabagCoreTestCase
         $this->assertContains('config.form_rss.description', $crawler->filter('body')->extract(['_text'])[0]);
     }
 
-    public function testLoginWith2Factor()
+    public function testLoginWith2FactorEmail()
     {
         $client = $this->getClient();
 
@@ -42,7 +42,7 @@ class SecurityControllerTest extends WallabagCoreTestCase
         $user = $em
             ->getRepository('WallabagUserBundle:User')
             ->findOneByUsername('admin');
-        $user->setTwoFactorAuthentication(true);
+        $user->setEmailTwoFactor(true);
         $em->persist($user);
         $em->flush();
 
@@ -54,12 +54,12 @@ class SecurityControllerTest extends WallabagCoreTestCase
         $user = $em
             ->getRepository('WallabagUserBundle:User')
             ->findOneByUsername('admin');
-        $user->setTwoFactorAuthentication(false);
+        $user->setEmailTwoFactor(false);
         $em->persist($user);
         $em->flush();
     }
 
-    public function testTrustedComputer()
+    public function testLoginWith2FactorGoogle()
     {
         $client = $this->getClient();
 
@@ -69,15 +69,27 @@ class SecurityControllerTest extends WallabagCoreTestCase
             return;
         }
 
+        $client->followRedirects();
+
         $em = $client->getContainer()->get('doctrine.orm.entity_manager');
         $user = $em
             ->getRepository('WallabagUserBundle:User')
             ->findOneByUsername('admin');
+        $user->setGoogleAuthenticatorSecret('26LDIHYGHNELOQEM');
+        $em->persist($user);
+        $em->flush();
+
+        $this->logInAsUsingHttp('admin');
+        $crawler = $client->request('GET', '/config');
+        $this->assertContains('scheb_two_factor.trusted', $crawler->filter('body')->extract(['_text'])[0]);
 
-        $date = new \DateTime();
-        $user->addTrustedComputer('ABCDEF', $date->add(new \DateInterval('P1M')));
-        $this->assertTrue($user->isTrustedComputer('ABCDEF'));
-        $this->assertFalse($user->isTrustedComputer('FEDCBA'));
+        // restore user
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUsername('admin');
+        $user->setGoogleAuthenticatorSecret(null);
+        $em->persist($user);
+        $em->flush();
     }
 
     public function testEnabledRegistration()
index 3dd9273c825f082032d0c9933dbff0995866ee81..508adb1b61f62a1723cf94f1c442ae77fc99e3ca 100644 (file)
@@ -163,7 +163,7 @@ class ContentProxyTest extends TestCase
 
         $this->assertSame('http://1.1.1.1', $entry->getUrl());
         $this->assertSame('this is my title', $entry->getTitle());
-        $this->assertContains('this is my content', $entry->getContent());
+        $this->assertContains('content', $entry->getContent());
         $this->assertSame('http://3.3.3.3/cover.jpg', $entry->getPreviewPicture());
         $this->assertSame('text/html', $entry->getMimetype());
         $this->assertSame('fr', $entry->getLanguage());
@@ -205,7 +205,7 @@ class ContentProxyTest extends TestCase
 
         $this->assertSame('http://1.1.1.1', $entry->getUrl());
         $this->assertSame('this is my title', $entry->getTitle());
-        $this->assertContains('this is my content', $entry->getContent());
+        $this->assertContains('content', $entry->getContent());
         $this->assertNull($entry->getPreviewPicture());
         $this->assertSame('text/html', $entry->getMimetype());
         $this->assertSame('fr', $entry->getLanguage());
@@ -247,7 +247,7 @@ class ContentProxyTest extends TestCase
 
         $this->assertSame('http://1.1.1.1', $entry->getUrl());
         $this->assertSame('this is my title', $entry->getTitle());
-        $this->assertContains('this is my content', $entry->getContent());
+        $this->assertContains('content', $entry->getContent());
         $this->assertSame('text/html', $entry->getMimetype());
         $this->assertNull($entry->getLanguage());
         $this->assertSame('200', $entry->getHttpStatus());
@@ -296,7 +296,7 @@ class ContentProxyTest extends TestCase
 
         $this->assertSame('http://1.1.1.1', $entry->getUrl());
         $this->assertSame('this is my title', $entry->getTitle());
-        $this->assertContains('this is my content', $entry->getContent());
+        $this->assertContains('content', $entry->getContent());
         $this->assertNull($entry->getPreviewPicture());
         $this->assertSame('text/html', $entry->getMimetype());
         $this->assertSame('fr', $entry->getLanguage());
@@ -332,7 +332,7 @@ class ContentProxyTest extends TestCase
 
         $this->assertSame('http://1.1.1.1', $entry->getUrl());
         $this->assertSame('this is my title', $entry->getTitle());
-        $this->assertContains('this is my content', $entry->getContent());
+        $this->assertContains('content', $entry->getContent());
         $this->assertSame('text/html', $entry->getMimetype());
         $this->assertSame('fr', $entry->getLanguage());
         $this->assertSame(4.0, $entry->getReadingTime());
@@ -371,7 +371,7 @@ class ContentProxyTest extends TestCase
 
         $this->assertSame('http://1.1.1.1', $entry->getUrl());
         $this->assertSame('this is my title', $entry->getTitle());
-        $this->assertContains('this is my content', $entry->getContent());
+        $this->assertContains('content', $entry->getContent());
         $this->assertSame('text/html', $entry->getMimetype());
         $this->assertSame('fr', $entry->getLanguage());
         $this->assertSame(4.0, $entry->getReadingTime());
@@ -406,7 +406,7 @@ class ContentProxyTest extends TestCase
 
         $this->assertSame('http://1.1.1.1', $entry->getUrl());
         $this->assertSame('this is my title', $entry->getTitle());
-        $this->assertContains('this is my content', $entry->getContent());
+        $this->assertContains('content', $entry->getContent());
         $this->assertSame('text/html', $entry->getMimetype());
         $this->assertSame('fr', $entry->getLanguage());
         $this->assertSame(4.0, $entry->getReadingTime());
index e34e13a8a29cf2a7bab12a7a163f57ea226114c8..1713c10c81329108534063ab2db39dad6bf3672e 100644 (file)
@@ -33,7 +33,7 @@ TWIG;
     public function testSendEmail()
     {
         $user = new User();
-        $user->setTwoFactorAuthentication(true);
+        $user->setEmailTwoFactor(true);
         $user->setEmailAuthCode(666666);
         $user->setEmail('test@wallabag.io');
         $user->setName('Bob');