diff --git a/src/Module/Security/Logout.php b/src/Module/Security/Logout.php index c698dd00b0..2a4b33e2f8 100644 --- a/src/Module/Security/Logout.php +++ b/src/Module/Security/Logout.php @@ -26,6 +26,7 @@ use Friendica\Core\Hook; use Friendica\Core\System; use Friendica\DI; use Friendica\Model\Profile; +use Friendica\Security\TwoFactor; /** * Logout module @@ -44,6 +45,13 @@ 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 (DI::cookie()->get('trusted')) { + $trustedBrowserRepository = new TwoFactor\Repository\TrustedBrowser(DI::dba(), DI::logger()); + $trustedBrowserRepository->removeForUser(local_user(), DI::cookie()->get('trusted')); + } + DI::cookie()->clear(); DI::session()->clear(); diff --git a/src/Module/Security/TwoFactor/Verify.php b/src/Module/Security/TwoFactor/Verify.php index d7a44f0c56..8e3c75c01c 100644 --- a/src/Module/Security/TwoFactor/Verify.php +++ b/src/Module/Security/TwoFactor/Verify.php @@ -26,6 +26,7 @@ use Friendica\Core\Renderer; use Friendica\Core\Session; use Friendica\DI; use PragmaRX\Google2FA\Google2FA; +use Friendica\Security\TwoFactor; /** * Page 1: Authenticator code verification @@ -55,6 +56,19 @@ class Verify extends BaseModule 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); + } + // Resume normal login workflow DI::auth()->setForUser($a, $a->user, true, true); } else { @@ -83,6 +97,7 @@ class Verify extends BaseModule '$errors' => self::$errors, '$recovery_message' => DI::l10n()->t('Don’t have your phone? Enter a two-factor recovery code', '2fa/recovery'), '$verify_code' => ['verify_code', DI::l10n()->t('Please enter a code from your authentication app'), '', '', DI::l10n()->t('Required'), 'autofocus autocomplete="off" placeholder="000000"', 'tel'], + '$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'), ]); } diff --git a/src/Security/Authentication.php b/src/Security/Authentication.php index 089035bb7f..a36341a1d8 100644 --- a/src/Security/Authentication.php +++ b/src/Security/Authentication.php @@ -33,7 +33,7 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\User; use Friendica\Network\HTTPException; -use Friendica\Repository\TwoFactor\TrustedBrowser; +use Friendica\Security\TwoFactor\Repository\TrustedBrowser; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; use Friendica\Util\Strings; @@ -427,11 +427,38 @@ class Authentication return; } - // Case 1: 2FA session present and valid: return + // Case 1a: 2FA session already present: return if ($this->session->get('2fa')) { return; } + // Case 1b: Check for trusted browser + if ($this->cookie->get('trusted')) { + // Retrieve a trusted_browser model based on cookie hash + $trustedBrowserRepository = new TrustedBrowser($this->dba, $this->logger); + try { + $trustedBrowser = $trustedBrowserRepository->selectOneByHash($this->cookie->get('trusted')); + // Verify record ownership + if ($trustedBrowser->uid === $uid) { + // Update last_used date + $trustedBrowser->recordUse(); + + // Save it to the database + $trustedBrowserRepository->save($trustedBrowser); + + // Set 2fa session key and return + $this->session->set('2fa', true); + + return; + } else { + // Invalid trusted cookie value, removing it + $this->cookie->unset('trusted'); + } + } catch (\Throwable $e) { + // Local trusted browser record was probably removed by the user, we carry on with 2FA + } + } + // Case 2: No valid 2FA session: redirect to code verification page if ($this->mode->isAjax()) { throw new HTTPException\ForbiddenException(); diff --git a/view/templates/twofactor/verify.tpl b/view/templates/twofactor/verify.tpl index 2b1fe31421..938f98da09 100644 --- a/view/templates/twofactor/verify.tpl +++ b/view/templates/twofactor/verify.tpl @@ -18,6 +18,8 @@ {{include file="field_input.tpl" field=$verify_code}} + {{include file="field_checkbox.tpl" field=$trust_browser}} +