]> git.mxchange.org Git - friendica.git/commitdiff
Improve 2 factor usage
authorPhilipp <admin@philipp.info>
Sat, 25 Jun 2022 12:45:33 +0000 (14:45 +0200)
committerPhilipp <admin@philipp.info>
Sat, 25 Jun 2022 21:04:00 +0000 (23:04 +0200)
20 files changed:
database.sql
doc/database/db_2fa_trusted_browser.md
src/Model/User/Cookie.php
src/Module/Security/Logout.php
src/Module/Security/TwoFactor/Signout.php [new file with mode: 0644]
src/Module/Security/TwoFactor/Trust.php [new file with mode: 0644]
src/Module/Security/TwoFactor/Verify.php
src/Module/Settings/TwoFactor/Index.php
src/Module/Settings/TwoFactor/Trusted.php
src/Security/Authentication.php
src/Security/TwoFactor/Factory/TrustedBrowser.php
src/Security/TwoFactor/Model/TrustedBrowser.php
static/dbstructure.config.php
static/routes.config.php
tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php
tests/src/Security/TwoFactor/Model/TrustedBrowserTest.php
view/templates/settings/twofactor/trusted_browsers.tpl
view/templates/twofactor/signout.tpl [new file with mode: 0644]
view/templates/twofactor/trust.tpl [new file with mode: 0644]
view/templates/twofactor/verify.tpl

index 10b18d4cae9e993baea6c60319ef9f41585545f7..01bd84b00b28ed58bc76f467e1478fbc3fba5d58 100644 (file)
@@ -1,6 +1,6 @@
 -- ------------------------------------------
 -- Friendica 2022.09-dev (Giant Rhubarb)
--- DB_UPDATE_VERSION 1472
+-- DB_UPDATE_VERSION 1473
 -- ------------------------------------------
 
 
@@ -297,6 +297,7 @@ CREATE TABLE IF NOT EXISTS `2fa_trusted_browser` (
        `cookie_hash` varchar(80) NOT NULL COMMENT 'Trusted cookie hash',
        `uid` mediumint unsigned NOT NULL COMMENT 'User ID',
        `user_agent` text COMMENT 'User agent string',
+       `trusted` boolean NOT NULL DEFAULT '1' COMMENT 'Whenever this browser should be trusted or not',
        `created` datetime NOT NULL COMMENT 'Datetime the trusted browser was recorded',
        `last_used` datetime COMMENT 'Datetime the trusted browser was last used',
         PRIMARY KEY(`cookie_hash`),
index d12d9e1fc0bbe38dc033521b1a59bb0ff17b9185..18126b49fc9f07d1f806128a6bf3488a620e4b4f 100644 (file)
@@ -6,13 +6,14 @@ Two-factor authentication trusted browsers
 Fields
 ------
 
-| Field       | Description                                | Type               | Null | Key | Default | Extra |
-| ----------- | ------------------------------------------ | ------------------ | ---- | --- | ------- | ----- |
-| cookie_hash | Trusted cookie hash                        | varchar(80)        | NO   | PRI | NULL    |       |
-| uid         | User ID                                    | mediumint unsigned | NO   |     | NULL    |       |
-| user_agent  | User agent string                          | text               | YES  |     | NULL    |       |
-| created     | Datetime the trusted browser was recorded  | datetime           | NO   |     | NULL    |       |
-| last_used   | Datetime the trusted browser was last used | datetime           | YES  |     | NULL    |       |
+| Field       | Description                                    | Type               | Null | Key | Default | Extra |
+| ----------- | ---------------------------------------------- | ------------------ | ---- | --- | ------- | ----- |
+| cookie_hash | Trusted cookie hash                            | varchar(80)        | NO   | PRI | NULL    |       |
+| uid         | User ID                                        | mediumint unsigned | NO   |     | NULL    |       |
+| user_agent  | User agent string                              | text               | YES  |     | NULL    |       |
+| trusted     | Whenever this browser should be trusted or not | boolean            | NO   |     | 1       |       |
+| created     | Datetime the trusted browser was recorded      | datetime           | NO   |     | NULL    |       |
+| last_used   | Datetime the trusted browser was last used     | datetime           | YES  |     | NULL    |       |
 
 Indexes
 ------------
index aabb0282094f35780ed39779a31d7a9797498c4a..4359d21071d73923185fc69ac9b9e2118e54c141 100644 (file)
@@ -124,6 +124,19 @@ class Cookie
                }
        }
 
+       /**
+        * Resets the cookie to a given data set
+        *
+        * @param array $data
+        *
+        * @return bool
+        */
+       public function reset(array $data): bool
+       {
+               return $this->clear() &&
+                          $this->setMultiple($data);
+       }
+
        /**
         * Clears the Friendica cookie
         */
@@ -131,7 +144,7 @@ class Cookie
        {
                $this->data = [];
                // make sure cookie is deleted on browser close, as a security measure
-               return $this->setCookie( '', -3600, $this->sslEnabled);
+               return $this->setCookie('', -3600, $this->sslEnabled);
        }
 
        /**
@@ -161,7 +174,7 @@ class Cookie
         *
         */
        protected function setCookie(string $value = null, int $expire = null,
-                                    bool $secure = null): bool
+                                                                bool $secure = null): bool
        {
                return setcookie(self::NAME, $value, $expire, self::PATH, self::DOMAIN, $secure, self::HTTPONLY);
        }
index 1ec076483440dc0658954148c9dff97dc1f6677e..004292cb5cce85d87812d9a0c879d8521aab6f1c 100644 (file)
@@ -31,7 +31,6 @@ use Friendica\Core\System;
 use Friendica\Model\Profile;
 use Friendica\Model\User\Cookie;
 use Friendica\Module\Response;
-use Friendica\Security\TwoFactor;
 use Friendica\Util\Profiler;
 use Psr\Log\LoggerInterface;
 
@@ -46,17 +45,14 @@ class Logout extends BaseModule
        protected $cookie;
        /** @var IHandleSessions */
        protected $session;
-       /** @var TwoFactor\Repository\TrustedBrowser */
-       protected $trustedBrowserRepo;
 
-       public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, TwoFactor\Repository\TrustedBrowser $trustedBrowserRepo, ICanCache $cache, Cookie $cookie, IHandleSessions $session, array $server, array $parameters = [])
+       public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, ICanCache $cache, Cookie $cookie, IHandleSessions $session, array $server, array $parameters = [])
        {
                parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
 
-               $this->cache              = $cache;
-               $this->cookie             = $cookie;
-               $this->session            = $session;
-               $this->trustedBrowserRepo = $trustedBrowserRepo;
+               $this->cache   = $cache;
+               $this->cookie  = $cookie;
+               $this->session = $session;
        }
 
 
@@ -73,9 +69,9 @@ class Logout extends BaseModule
 
                Hook::callAll("logging_out");
 
-               // Remove this trusted browser as it won't be able to be used ever again after the cookie is cleared
-               if ($this->cookie->get('trusted')) {
-                       $this->trustedBrowserRepo->removeForUser(local_user(), $this->cookie->get('trusted'));
+               // If this is a trusted browser, redirect to the 2fa signout page
+               if ($this->cookie->get('2fa_cookie_hash')) {
+                       $this->baseUrl->redirect('2fa/signout');
                }
 
                $this->cookie->clear();
diff --git a/src/Module/Security/TwoFactor/Signout.php b/src/Module/Security/TwoFactor/Signout.php
new file mode 100644 (file)
index 0000000..3e52b27
--- /dev/null
@@ -0,0 +1,129 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2022, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Module\Security\TwoFactor;
+
+use Friendica\App;
+use Friendica\BaseModule;
+use Friendica\Core\L10n;
+use Friendica\Core\Renderer;
+use Friendica\Core\Session\Capability\IHandleSessions;
+use Friendica\Model\User\Cookie;
+use Friendica\Module\Response;
+use Friendica\Network\HTTPException\NotFoundException;
+use Friendica\Util\Profiler;
+use Friendica\Security\TwoFactor;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Page 4: Logout dialog for trusted browsers
+ *
+ * @package Friendica\Module\TwoFactor
+ */
+class Signout extends BaseModule
+{
+       protected $errors = [];
+
+       /** @var IHandleSessions  */
+       protected $session;
+       /** @var Cookie  */
+       protected $cookie;
+       /** @var TwoFactor\Repository\TrustedBrowser  */
+       protected $trustedBrowserRepositoy;
+
+       public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger,  IHandleSessions $session, Cookie $cookie, TwoFactor\Repository\TrustedBrowser $trustedBrowserRepositoy, Profiler $profiler, Response $response, array $server, array $parameters = [])
+       {
+               parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
+
+               $this->session                 = $session;
+               $this->cookie                  = $cookie;
+               $this->trustedBrowserRepositoy = $trustedBrowserRepositoy;
+       }
+
+       protected function post(array $request = [])
+       {
+               if (!local_user() || !($this->cookie->get('2fa_cookie_hash'))) {
+                       return;
+               }
+
+               $action = $request['action'] ?? '';
+
+               if (!empty($action)) {
+                       self::checkFormSecurityTokenRedirectOnError('2fa', 'twofactor_signout');
+
+                       switch ($action) {
+                               case 'trust_and_sign_out':
+                                       $trusted = $this->cookie->get('2fa_cookie_hash');
+                                       $this->cookie->reset(['2fa_cookie_hash' => $trusted]);
+                                       $this->session->clear();
+
+                                       info($this->t('Logged out.'));
+                                       $this->baseUrl->redirect();
+                                       break;
+                               case 'sign_out':
+                                       $this->trustedBrowserRepositoy->removeForUser(local_user(), $this->cookie->get('2fa_cookie_hash'));
+                                       $this->cookie->clear();
+                                       $this->session->clear();
+
+                                       info($this->t('Logged out.'));
+                                       $this->baseUrl->redirect();
+                                       break;
+                               default:
+                                       $this->baseUrl->redirect();
+                       }
+               }
+       }
+
+       protected function content(array $request = []): string
+       {
+               if (!local_user() || !($this->cookie->get('2fa_cookie_hash'))) {
+                       $this->baseUrl->redirect();
+               }
+
+               try {
+                       $trustedBrowser = $this->trustedBrowserRepositoy->selectOneByHash($this->cookie->get('2fa_cookie_hash'));
+                       if (!$trustedBrowser->trusted) {
+                               $trusted = $this->cookie->get('2fa_cookie_hash');
+                               $this->cookie->reset(['2fa_cookie_hash' => $trusted]);
+                               $this->session->clear();
+
+                               info($this->t('Logged out.'));
+                               $this->baseUrl->redirect();
+                       }
+               } catch (NotFoundException $exception) {
+                       $this->cookie->clear();
+                       $this->session->clear();
+
+                       info($this->t('Logged out.'));
+                       $this->baseUrl->redirect();
+               }
+
+               return Renderer::replaceMacros(Renderer::getMarkupTemplate('twofactor/signout.tpl'), [
+                       '$form_security_token' => self::getFormSecurityToken('twofactor_signout'),
+
+                       '$title'                    => $this->t('Sign out of this browser?'),
+                       '$message'                  => $this->t('<p>If you trust this browser, you will not be asked for verification code the next time you sign in.</p>'),
+                       '$sign_out_label'           => $this->t('Sign out'),
+                       '$cancel_label'             => $this->t('Cancel'),
+                       '$trust_and_sign_out_label' => $this->t('Trust and sign out'),
+               ]);
+       }
+}
diff --git a/src/Module/Security/TwoFactor/Trust.php b/src/Module/Security/TwoFactor/Trust.php
new file mode 100644 (file)
index 0000000..a245e6b
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2022, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Module\Security\TwoFactor;
+
+use Friendica\App;
+use Friendica\BaseModule;
+use Friendica\Core\L10n;
+use Friendica\Core\Renderer;
+use Friendica\Core\Session\Capability\IHandleSessions;
+use Friendica\Model\User;
+use Friendica\Model\User\Cookie;
+use Friendica\Module\Response;
+use Friendica\Network\HTTPException\NotFoundException;
+use Friendica\Security\Authentication;
+use Friendica\Util\Profiler;
+use Friendica\Security\TwoFactor;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Page 2: Trust Browser dialog
+ *
+ * @package Friendica\Module\TwoFactor
+ */
+class Trust extends BaseModule
+{
+       /** @var App  */
+       protected $app;
+       /** @var Authentication  */
+       protected $auth;
+       /** @var IHandleSessions  */
+       protected $session;
+       /** @var Cookie  */
+       protected $cookie;
+       /** @var TwoFactor\Factory\TrustedBrowser  */
+       protected $trustedBrowserFactory;
+       /** @var TwoFactor\Repository\TrustedBrowser  */
+       protected $trustedBrowserRepositoy;
+
+       public function __construct(App $app, Authentication $auth, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, IHandleSessions $session, Cookie $cookie, TwoFactor\Factory\TrustedBrowser $trustedBrowserFactory, TwoFactor\Repository\TrustedBrowser $trustedBrowserRepositoy, Response $response, array $server, array $parameters = [])
+       {
+               parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
+
+               $this->app                     = $app;
+               $this->auth                    = $auth;
+               $this->session                 = $session;
+               $this->cookie                  = $cookie;
+               $this->trustedBrowserFactory   = $trustedBrowserFactory;
+               $this->trustedBrowserRepositoy = $trustedBrowserRepositoy;
+       }
+
+       protected function post(array $request = [])
+       {
+               if (!local_user() || !$this->session->get('2fa')) {
+                       return;
+               }
+
+               $action = $request['action'] ?? '';
+
+               if (!empty($action)) {
+                       self::checkFormSecurityTokenRedirectOnError('2fa', 'twofactor_trust');
+
+                       switch ($action) {
+                               case 'trust':
+                               case 'dont_trust':
+                                       $trustedBrowser = $this->trustedBrowserFactory->createForUserWithUserAgent(local_user(), $this->server['HTTP_USER_AGENT'], $action === 'trust');
+                                       $this->trustedBrowserRepositoy->save($trustedBrowser);
+
+                                       // The string is sent to the browser to be sent back with each request
+                                       if (!$this->cookie->set('2fa_cookie_hash', $trustedBrowser->cookie_hash)) {
+                                               notice($this->t('Couldn\'t save browser to Cookie.'));
+                                       };
+                                       break;
+                       }
+
+                       $this->auth->setForUser($this->app, User::getById($this->app->getLoggedInUserId()), true, true);
+               }
+       }
+
+       protected function content(array $request = []): string
+       {
+               if (!local_user() || !$this->session->get('2fa')) {
+                       $this->baseUrl->redirect();
+               }
+
+               if ($this->cookie->get('2fa_cookie_hash')) {
+                       try {
+                               $trustedBrowser = $this->trustedBrowserRepositoy->selectOneByHash($this->cookie->get('2fa_cookie_hash'));
+                               if (!$trustedBrowser->trusted) {
+                                       $this->auth->setForUser($this->app, User::getById($this->app->getLoggedInUserId()), true, true);
+                                       $this->baseUrl->redirect();
+                               }
+                       } catch (NotFoundException $exception) {
+                               $this->logger->notice('Trusted Browser of the cookie not found.', ['cookie_hash' => $this->cookie->get('trusted'), 'uid' => $this->app->getLoggedInUserId(), 'exception' => $exception]);
+                       }
+               }
+
+               return Renderer::replaceMacros(Renderer::getMarkupTemplate('twofactor/trust.tpl'), [
+                       '$form_security_token' => self::getFormSecurityToken('twofactor_trust'),
+
+                       '$title'            => $this->t('Trust this browser?'),
+                       '$message'          => $this->t('<p>If you choose to trust this browser, you will not be asked for a verification code the next time you sign in.</p>'),
+                       '$not_now_label'    => $this->t('Not now'),
+                       '$dont_trust_label' => $this->t('Don\'t trust'),
+                       '$trust_label'      => $this->t('Trust'),
+               ]);
+       }
+}
index b102823576837112a8a673e557c17248091ed627..d0850aedcbae63006d665b8002f51f5f5b16c48a 100644 (file)
 
 namespace Friendica\Module\Security\TwoFactor;
 
+use Friendica\App;
 use Friendica\BaseModule;
+use Friendica\Core\L10n;
+use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues;
 use Friendica\Core\Renderer;
-use Friendica\Core\Session;
-use Friendica\DI;
-use Friendica\Model\User;
+use Friendica\Core\Session\Capability\IHandleSessions;
+use Friendica\Module\Response;
+use Friendica\Util\Profiler;
 use PragmaRX\Google2FA\Google2FA;
 use Friendica\Security\TwoFactor;
+use Psr\Log\LoggerInterface;
 
 /**
  * Page 1: Authenticator code verification
@@ -36,7 +40,20 @@ use Friendica\Security\TwoFactor;
  */
 class Verify extends BaseModule
 {
-       private static $errors = [];
+       protected $errors = [];
+
+       /** @var IHandleSessions  */
+       protected $session;
+       /** @var IManagePersonalConfigValues  */
+       protected $pConfig;
+
+       public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IManagePersonalConfigValues $pConfig, IHandleSessions $session, array $server, array $parameters = [])
+       {
+               parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
+
+               $this->session = $session;
+               $this->pConfig = $pConfig;
+       }
 
        protected function post(array $request = [])
        {
@@ -44,36 +61,20 @@ class Verify extends BaseModule
                        return;
                }
 
-               if (($_POST['action'] ?? '') == 'verify') {
+               if (($request['action'] ?? '') === 'verify') {
                        self::checkFormSecurityTokenRedirectOnError('2fa', 'twofactor_verify');
 
-                       $a = DI::app();
+                       $code = $request['verify_code'] ?? '';
 
-                       $code = $_POST['verify_code'] ?? '';
-
-                       $valid = (new Google2FA())->verifyKey(DI::pConfig()->get(local_user(), '2fa', 'secret'), $code);
+                       $valid = (new Google2FA())->verifyKey($this->pConfig->get(local_user(), '2fa', 'secret'), $code);
 
                        // The same code can't be used twice even if it's valid
-                       if ($valid && Session::get('2fa') !== $code) {
-                               Session::set('2fa', $code);
-
-                               // Trust this browser feature
-                               if (!empty($_REQUEST['trust_browser'])) {
-                                       $trustedBrowserFactory = new TwoFactor\Factory\TrustedBrowser(DI::logger());
-                                       $trustedBrowserRepository = new TwoFactor\Repository\TrustedBrowser(DI::dba(), DI::logger(), $trustedBrowserFactory);
-
-                                       $trustedBrowser = $trustedBrowserFactory->createForUserWithUserAgent(local_user(), $_SERVER['HTTP_USER_AGENT']);
-
-                                       $trustedBrowserRepository->save($trustedBrowser);
-
-                                       // The string is sent to the browser to be sent back with each request
-                                       DI::cookie()->set('trusted', $trustedBrowser->cookie_hash);
-                               }
+                       if ($valid && $this->session->get('2fa') !== $code) {
+                               $this->session->set('2fa', $code);
 
-                               // Resume normal login workflow
-                               DI::auth()->setForUser($a, User::getById($a->getLoggedInUserId()), true, true);
+                               $this->baseUrl->redirect('2fa/trust');
                        } else {
-                               self::$errors[] = DI::l10n()->t('Invalid code, please retry.');
+                               $this->errors[] = $this->t('Invalid code, please retry.');
                        }
                }
        }
@@ -81,25 +82,24 @@ class Verify extends BaseModule
        protected function content(array $request = []): string
        {
                if (!local_user()) {
-                       DI::baseUrl()->redirect();
+                       $this->baseUrl->redirect();
                }
 
                // Already authenticated with 2FA token
-               if (Session::get('2fa')) {
-                       DI::baseUrl()->redirect();
+               if ($this->session->get('2fa')) {
+                       $this->baseUrl->redirect();
                }
 
                return Renderer::replaceMacros(Renderer::getMarkupTemplate('twofactor/verify.tpl'), [
                        '$form_security_token' => self::getFormSecurityToken('twofactor_verify'),
 
-                       '$title'            => DI::l10n()->t('Two-factor authentication'),
-                       '$message'          => DI::l10n()->t('<p>Open the two-factor authentication app on your device to get an authentication code and verify your identity.</p>'),
-                       '$errors_label'     => DI::l10n()->tt('Error', 'Errors', count(self::$errors)),
-                       '$errors'           => self::$errors,
-                       '$recovery_message' => DI::l10n()->t('If you do not have access to your authentication code you can use <a href="%s">a two-factor recovery code</a>.', '2fa/recovery'),
-                       '$verify_code'      => ['verify_code', DI::l10n()->t('Please enter a code from your authentication app'), '', '', DI::l10n()->t('Required'), 'autofocus autocomplete="one-time-code" placeholder="000000" inputmode="numeric" pattern="[0-9]*"'],
-                       '$trust_browser'    => ['trust_browser', DI::l10n()->t('This is my two-factor authenticator app device'), !empty($_REQUEST['trust_browser'])],
-                       '$verify_label'     => DI::l10n()->t('Verify code and complete login'),
+                       '$title'            => $this->t('Two-factor authentication'),
+                       '$message'          => $this->t('<p>Open the two-factor authentication app on your device to get an authentication code and verify your identity.</p>'),
+                       '$errors_label'     => $this->tt('Error', 'Errors', count($this->errors)),
+                       '$errors'           => $this->errors,
+                       '$recovery_message' => $this->t('If you do not have access to your authentication code you can use a <a href="%s">two-factor recovery code</a>.', '2fa/recovery'),
+                       '$verify_code'      => ['verify_code', $this->t('Please enter a code from your authentication app'), '', '', $this->t('Required'), 'autofocus autocomplete="one-time-code" placeholder="000000" inputmode="numeric" pattern="[0-9]*"'],
+                       '$verify_label'     => $this->t('Verify code and complete login'),
                ]);
        }
 }
index 35c5d3cf9c19abd30ff9cecf18cbf6c54812c8dc..98826824b9eb703c60e18d78dc15085ca73987f0 100644 (file)
@@ -24,6 +24,7 @@ namespace Friendica\Module\Settings\TwoFactor;
 use Friendica\Core\Renderer;
 use Friendica\Core\Session;
 use Friendica\DI;
+use Friendica\Network\HTTPException\FoundException;
 use Friendica\Security\TwoFactor\Model\AppSpecificPassword;
 use Friendica\Security\TwoFactor\Model\RecoveryCode;
 use Friendica\Model\User;
@@ -90,7 +91,9 @@ class Index extends BaseSettings
                                        break;
                        }
                } catch (\Exception $e) {
-                       notice(DI::l10n()->t('Wrong Password'));
+                       if (!($e instanceof FoundException)) {
+                               notice(DI::l10n()->t($e->getMessage()));
+                       }
                }
        }
 
index 12327a5918961af3875de186faa5d6b48acb23f7..d1c0476de5dca115c4abdf956b801f9349914988 100644 (file)
@@ -121,6 +121,7 @@ class Trusted extends BaseSettings
                                'os' => $result->os->family,
                                'device' => $result->device->family,
                                'browser' => $result->ua->family,
+                               'trusted_labeled' => $trustedBrowser->trusted ? $this->t('Yes') : $this->t('No'),
                        ];
 
                        return $trustedBrowser->toArray() + $dates + $uaData;
@@ -135,7 +136,8 @@ class Trusted extends BaseSettings
                        '$device_label'        => $this->t('Device'),
                        '$os_label'            => $this->t('OS'),
                        '$browser_label'       => $this->t('Browser'),
-                       '$created_label'       => $this->t('Trusted'),
+                       '$trusted_label'       => $this->t('Trusted'),
+                       '$created_label'       => $this->t('Created At'),
                        '$last_used_label'     => $this->t('Last Use'),
                        '$remove_label'        => $this->t('Remove'),
                        '$remove_all_label'    => $this->t('Remove All'),
index aca4f2c23e1132ef1fe3e5effd5428d2315cb85b..a23d0c95579c29359a9615729a573467817e3e67 100644 (file)
@@ -144,7 +144,7 @@ class Authentication
                                // Renew the cookie
                                $this->cookie->send();
 
-                               // Do the authentification if not done by now
+                               // Do the authentication if not done by now
                                if (!$this->session->get('authenticated')) {
                                        $this->setForUser($a, $user);
 
@@ -269,7 +269,11 @@ class Authentication
                }
 
                if (!$remember) {
+                       $trusted = $this->cookie->get('2fa_cookie_hash') ?? null;
                        $this->cookie->clear();
+                       if ($trusted) {
+                               $this->cookie->set('2fa_cookie_hash', $trusted);
+                       }
                }
 
                // if we haven't failed up this point, log them in.
@@ -407,11 +411,11 @@ class Authentication
                }
 
                // Case 1b: Check for trusted browser
-               if ($this->cookie->get('trusted')) {
+               if ($this->cookie->get('2fa_cookie_hash')) {
                        // Retrieve a trusted_browser model based on cookie hash
                        $trustedBrowserRepository = new TrustedBrowser($this->dba, $this->logger);
                        try {
-                               $trustedBrowser = $trustedBrowserRepository->selectOneByHash($this->cookie->get('trusted'));
+                               $trustedBrowser = $trustedBrowserRepository->selectOneByHash($this->cookie->get('2fa_cookie_hash'));
                                // Verify record ownership
                                if ($trustedBrowser->uid === $uid) {
                                        // Update last_used date
@@ -420,10 +424,13 @@ class Authentication
                                        // Save it to the database
                                        $trustedBrowserRepository->save($trustedBrowser);
 
-                                       // Set 2fa session key and return
-                                       $this->session->set('2fa', true);
+                                       // Only use this entry, if its really trusted, otherwise just update the record and proceed
+                                       if ($trustedBrowser->trusted) {
+                                               // Set 2fa session key and return
+                                               $this->session->set('2fa', true);
 
-                                       return;
+                                               return;
+                                       }
                                } else {
                                        // Invalid trusted cookie value, removing it
                                        $this->cookie->unset('trusted');
index 61ec154fcc23b9989085bd2c86c26751289a6c5e..21961f7ef368b54b5067d0a8182046bdcb3690c5 100644 (file)
@@ -27,7 +27,7 @@ use Friendica\Util\Strings;
 
 class TrustedBrowser extends BaseFactory
 {
-       public function createForUserWithUserAgent($uid, $userAgent): \Friendica\Security\TwoFactor\Model\TrustedBrowser
+       public function createForUserWithUserAgent(int $uid, string $userAgent, bool $trusted): \Friendica\Security\TwoFactor\Model\TrustedBrowser
        {
                $trustedHash = Strings::getRandomHex();
 
@@ -35,6 +35,7 @@ class TrustedBrowser extends BaseFactory
                        $trustedHash,
                        $uid,
                        $userAgent,
+                       $trusted,
                        DateTimeFormat::utcNow()
                );
        }
@@ -45,6 +46,7 @@ class TrustedBrowser extends BaseFactory
                        $row['cookie_hash'],
                        $row['uid'],
                        $row['user_agent'],
+                       $row['trusted'],
                        $row['created'],
                        $row['last_used']
                );
index d0a654d5f282045884654fa833636b855e1bdc44..cd9d2007e6f18fd02a3ac2f4b91b633d0a2e2ed8 100644 (file)
@@ -31,6 +31,7 @@ use Friendica\Util\DateTimeFormat;
  * @property-read $cookie_hash
  * @property-read $uid
  * @property-read $user_agent
+ * @property-read $trusted
  * @property-read $created
  * @property-read $last_used
  * @package Friendica\Model\TwoFactor
@@ -40,6 +41,7 @@ class TrustedBrowser extends BaseEntity
        protected $cookie_hash;
        protected $uid;
        protected $user_agent;
+       protected $trusted;
        protected $created;
        protected $last_used;
 
@@ -51,16 +53,18 @@ class TrustedBrowser extends BaseEntity
         * @param string      $cookie_hash
         * @param int         $uid
         * @param string      $user_agent
+        * @param bool        $trusted
         * @param string      $created
         * @param string|null $last_used
         */
-       public function __construct(string $cookie_hash, int $uid, string $user_agent, string $created, string $last_used = null)
+       public function __construct(string $cookie_hash, int $uid, string $user_agent, bool $trusted, string $created, string $last_used = null)
        {
                $this->cookie_hash = $cookie_hash;
-               $this->uid = $uid;
-               $this->user_agent = $user_agent;
-               $this->created = $created;
-               $this->last_used = $last_used;
+               $this->uid         = $uid;
+               $this->user_agent  = $user_agent;
+               $this->trusted     = $trusted;
+               $this->created     = $created;
+               $this->last_used   = $last_used;
        }
 
        public function recordUse()
index c48da6a09a12f39985e40d60bf29a09b3c3e5403..758c33d0dac1791b3b1e779b77c54db5739022b9 100644 (file)
@@ -55,7 +55,7 @@
 use Friendica\Database\DBA;
 
 if (!defined('DB_UPDATE_VERSION')) {
-       define('DB_UPDATE_VERSION', 1472);
+       define('DB_UPDATE_VERSION', 1473);
 }
 
 return [
@@ -358,6 +358,7 @@ return [
                        "cookie_hash" => ["type" => "varchar(80)", "not null" => "1", "primary" => "1", "comment" => "Trusted cookie hash"],
                        "uid" => ["type" => "mediumint unsigned", "not null" => "1", "foreign" => ["user" => "uid"], "comment" => "User ID"],
                        "user_agent" => ["type" => "text", "comment" => "User agent string"],
+                       "trusted" => ["type" => "boolean", "not null" => "1", "default" => "1", "comment" => "Whenever this browser should be trusted or not"],
                        "created" => ["type" => "datetime", "not null" => "1", "comment" => "Datetime the trusted browser was recorded"],
                        "last_used" => ["type" => "datetime", "comment" => "Datetime the trusted browser was last used"],
                ],
index 72964d23080ed74b2cb9da199c8e63bad45837e2..9c82c8e1f14a42cfc38c42b83583d3f51f1aee89 100644 (file)
@@ -165,6 +165,8 @@ return [
        '/2fa' => [
                '[/]'       => [Module\Security\TwoFactor\Verify::class,   [R::GET, R::POST]],
                '/recovery' => [Module\Security\TwoFactor\Recovery::class, [R::GET, R::POST]],
+               '/trust'    => [Module\Security\TwoFactor\Trust::class,    [R::GET, R::POST]],
+               '/signout'  => [Module\Security\TwoFactor\Signout::class,  [R::GET, R::POST]],
        ],
 
        '/api' => [
index 84a118eb80b003b57690e5c324718a49168c0377..baddfb3791043a4c1664a95c42570fa619cef649 100644 (file)
@@ -38,6 +38,7 @@ class TrustedBrowserTest extends MockedTest
                        'uid' => 42,
                        'user_agent' => 'PHPUnit',
                        'created' => DateTimeFormat::utcNow(),
+                       'trusted' => true,
                        'last_used' => null,
                ];
 
@@ -57,6 +58,7 @@ class TrustedBrowserTest extends MockedTest
                        'uid' => null,
                        'user_agent' => null,
                        'created' => null,
+                       'trusted' => true,
                        'last_used' => null,
                ];
 
@@ -72,11 +74,12 @@ class TrustedBrowserTest extends MockedTest
                $uid = 42;
                $userAgent = 'PHPUnit';
 
-               $trustedBrowser = $factory->createForUserWithUserAgent($uid, $userAgent);
+               $trustedBrowser = $factory->createForUserWithUserAgent($uid, $userAgent, true);
 
                $this->assertNotEmpty($trustedBrowser->cookie_hash);
                $this->assertEquals($uid, $trustedBrowser->uid);
                $this->assertEquals($userAgent, $trustedBrowser->user_agent);
+               $this->assertTrue($trustedBrowser->trusted);
                $this->assertNotEmpty($trustedBrowser->created);
        }
 }
index 0c91ea5e5ea90a2707f87567b38ba26f72d94a87..cf5db0ffd8a6183c77d498ee2aa8de89b0d77dea 100644 (file)
@@ -36,12 +36,14 @@ class TrustedBrowserTest extends MockedTest
                        $hash,
                        42,
                        'PHPUnit',
+                       true,
                        DateTimeFormat::utcNow()
                );
 
                $this->assertEquals($hash, $trustedBrowser->cookie_hash);
                $this->assertEquals(42, $trustedBrowser->uid);
                $this->assertEquals('PHPUnit', $trustedBrowser->user_agent);
+               $this->assertTrue($trustedBrowser->trusted);
                $this->assertNotEmpty($trustedBrowser->created);
        }
 
@@ -54,6 +56,7 @@ class TrustedBrowserTest extends MockedTest
                        $hash,
                        42,
                        'PHPUnit',
+                       true,
                        $past,
                        $past
                );
index 29d2ab29c6d87f8d42d4250e88ec9cc1d71e67fb..1a9ae91fe57e43831f163d48506101515c95aa7d 100644 (file)
@@ -10,6 +10,7 @@
                                        <th>{{$device_label}}</th>
                                        <th>{{$os_label}}</th>
                                        <th>{{$browser_label}}</th>
+                                       <th>{{$trusted_label}}</th>
                                        <th>{{$created_label}}</th>
                                        <th>{{$last_used_label}}</th>
                                        <th><button type="submit" name="action" class="btn btn-primary btn-small" value="remove_all">{{$remove_all_label}}</button></th>
@@ -28,6 +29,9 @@
                                {{$trusted_browser.browser}}
                                        </td>
                                        <td>
+                        {{$trusted_browser.trusted_labeled}}
+                                       </td>
+                                       <td>
                                                <time class="time" title="{{$trusted_browser.created_local}}" data-toggle="tooltip" datetime="{{$trusted_browser.created_utc}}">{{$trusted_browser.created_ago}}</time>
                                        </td>
                                        <td>
diff --git a/view/templates/twofactor/signout.tpl b/view/templates/twofactor/signout.tpl
new file mode 100644 (file)
index 0000000..f2a2111
--- /dev/null
@@ -0,0 +1,14 @@
+<div class="generic-page-wrapper">
+       <h1>{{$title}}</h1>
+       <div>{{$message nofilter}}</div>
+
+       <form action="" method="post">
+               <input type="hidden" name="form_security_token" value="{{$form_security_token}}">
+
+               <div class="form-group settings-submit-wrapper">
+                       <button type="submit" name="action" id="trust-submit-button" class="btn btn-primary confirm-button" value="sign_out">{{$sign_out_label}}</button>
+                       <button type="submit" name="action" id="dont-trust-submit-button" class="btn confirm-button" value="cancel">{{$cancel_label}}</button>
+                       <button type="submit" name="action" id="not-now-submit-button" class="right-aligned btn confirm-button" value="trust_and_sign_out">{{$trust_and_sign_out_label}}</button>
+               </div>
+       </form>
+</div>
diff --git a/view/templates/twofactor/trust.tpl b/view/templates/twofactor/trust.tpl
new file mode 100644 (file)
index 0000000..6bfe307
--- /dev/null
@@ -0,0 +1,14 @@
+<div class="generic-page-wrapper">
+       <h1>{{$title}}</h1>
+       <div>{{$message nofilter}}</div>
+
+       <form action="" method="post">
+               <input type="hidden" name="form_security_token" value="{{$form_security_token}}">
+
+               <div class="form-group settings-submit-wrapper">
+                       <button type="submit" name="action" id="trust-submit-button" class="btn btn-primary confirm-button" value="trust">{{$trust_label}}</button>
+                       <button type="submit" name="action" id="dont-trust-submit-button" class="btn confirm-button" value="dont_trust">{{$dont_trust_label}}</button>
+                       <button type="submit" name="action" id="not-now-submit-button" class="right-aligned btn confirm-button" value="not_now_label">{{$not_now_label}}</button>
+               </div>
+       </form>
+</div>
index 938f98da091ede5a860f2d10e9535e93cd372643..2b1fe31421f2785ba5ee47634ca8d8bcc54926bf 100644 (file)
@@ -18,8 +18,6 @@
 
                {{include file="field_input.tpl" field=$verify_code}}
 
-               {{include file="field_checkbox.tpl" field=$trust_browser}}
-
                <div class="form-group settings-submit-wrapper">
                        <button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="verify">{{$verify_label}}</button>
                </div>