Added support for trusted browser during authentication
authorHypolite Petovan <hypolite@mrpetovan.com>
Tue, 19 Jan 2021 04:32:48 +0000 (23:32 -0500)
committerHypolite Petovan <hypolite@mrpetovan.com>
Sat, 23 Jan 2021 10:42:59 +0000 (05:42 -0500)
src/Module/Security/Logout.php
src/Module/Security/TwoFactor/Verify.php
src/Security/Authentication.php
view/templates/twofactor/verify.tpl

index c698dd0..2a4b33e 100644 (file)
@@ -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();
 
index d7a44f0..8e3c75c 100644 (file)
@@ -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? <a href="%s">Enter 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="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'),
                ]);
        }
index 089035b..a36341a 100644 (file)
@@ -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();
index 2b1fe31..938f98d 100644 (file)
@@ -18,6 +18,8 @@
 
                {{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>