diff --git a/doc/Two-Factor-Authentication.md b/doc/Two-Factor-Authentication.md index eca6f598c9..2ec33ed29c 100644 --- a/doc/Two-Factor-Authentication.md +++ b/doc/Two-Factor-Authentication.md @@ -71,4 +71,11 @@ Just copy and paste it in your third-party app in the Friendica account password We recommend generating a single app-specific password for each separate third-party app you are using, using a meaningul description of the target app (like "Frienqa on my Fairphone 2"). You can also revoke any and all app-specific password you generated this way. -This may log you out of the third-party application(s) you used the revoked app-specific password to log in with. \ No newline at end of file +This may log you out of the third-party application(s) you used the revoked app-specific password to log in with. + +## Trusted browsers + +As a convenience, during two-factor authentication it is possible to identify a browser as trusted. +This will skip all further two-factor authentication prompt on this browser. + +You can remove any or all of these trusted browsers in the two-factor authentication settings. diff --git a/src/Module/Settings/TwoFactor/Index.php b/src/Module/Settings/TwoFactor/Index.php index bd3840485f..81b4639c78 100644 --- a/src/Module/Settings/TwoFactor/Index.php +++ b/src/Module/Settings/TwoFactor/Index.php @@ -78,6 +78,11 @@ class Index extends BaseSettings DI::baseUrl()->redirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password')); } break; + case 'trusted': + if ($has_secret) { + DI::baseUrl()->redirect('settings/2fa/trusted?t=' . self::getFormSecurityToken('settings_2fa_password')); + } + break; case 'configure': if (!$verified) { DI::baseUrl()->redirect('settings/2fa/verify?t=' . self::getFormSecurityToken('settings_2fa_password')); @@ -130,6 +135,7 @@ class Index extends BaseSettings '$disable_label' => DI::l10n()->t('Disable two-factor authentication'), '$recovery_codes_label' => DI::l10n()->t('Show recovery codes'), '$app_specific_passwords_label' => DI::l10n()->t('Manage app-specific passwords'), + '$trusted_browsers_label' => DI::l10n()->t('Manage trusted browsers'), '$configure_label' => DI::l10n()->t('Finish app configuration'), ]); } diff --git a/src/Module/Settings/TwoFactor/Trusted.php b/src/Module/Settings/TwoFactor/Trusted.php new file mode 100644 index 0000000000..7532509417 --- /dev/null +++ b/src/Module/Settings/TwoFactor/Trusted.php @@ -0,0 +1,110 @@ +get(local_user(), '2fa', 'verified'); + + if (!$verified) { + DI::baseUrl()->redirect('settings/2fa'); + } + + if (!self::checkFormSecurityToken('settings_2fa_password', 't')) { + notice(DI::l10n()->t('Please enter your password to access this page.')); + DI::baseUrl()->redirect('settings/2fa'); + } + } + + public static function post(array $parameters = []) + { + if (!local_user()) { + return; + } + + $trustedBrowserRepository = new TwoFactor\Repository\TrustedBrowser(DI::dba(), DI::logger()); + + if (!empty($_POST['action'])) { + self::checkFormSecurityTokenRedirectOnError('settings/2fa/trusted', 'settings_2fa_trusted'); + + switch ($_POST['action']) { + case 'remove_all' : + $trustedBrowserRepository->removeAllForUser(local_user()); + info(DI::l10n()->t('Trusted browsers successfully removed.')); + DI::baseUrl()->redirect('settings/2fa/trusted?t=' . self::getFormSecurityToken('settings_2fa_password')); + break; + } + } + + if (!empty($_POST['remove_id'])) { + self::checkFormSecurityTokenRedirectOnError('settings/2fa/trusted', 'settings_2fa_trusted'); + + if ($trustedBrowserRepository->removeForUser(local_user(), $_POST['remove_id'])) { + info(DI::l10n()->t('Trusted browser successfully removed.')); + } + + DI::baseUrl()->redirect('settings/2fa/trusted?t=' . self::getFormSecurityToken('settings_2fa_password')); + } + } + + + public static function content(array $parameters = []): string + { + parent::content($parameters); + + $trustedBrowserRepository = new TwoFactor\Repository\TrustedBrowser(DI::dba(), DI::logger()); + $trustedBrowsers = $trustedBrowserRepository->selectAllByUid(local_user()); + + $parser = Parser::create(); + + $trustedBrowserDisplay = array_map(function (TwoFactor\Model\TrustedBrowser $trustedBrowser) use ($parser) { + $dates = [ + 'created_ago' => Temporal::getRelativeDate($trustedBrowser->created), + 'last_used_ago' => Temporal::getRelativeDate($trustedBrowser->last_used), + ]; + + $result = $parser->parse($trustedBrowser->user_agent); + + $uaData = [ + 'os' => $result->os->family, + 'device' => $result->device->family, + 'browser' => $result->ua->family, + ]; + + return $trustedBrowser->toArray() + $dates + $uaData; + }, $trustedBrowsers->getArrayCopy()); + + return Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/twofactor/trusted_browsers.tpl'), [ + '$form_security_token' => self::getFormSecurityToken('settings_2fa_trusted'), + '$password_security_token' => self::getFormSecurityToken('settings_2fa_password'), + + '$title' => DI::l10n()->t('Two-factor Trusted Browsers'), + '$message' => DI::l10n()->t('Trusted browsers are individual browsers you chose to skip two-factor authentication to access Friendica. Please use this feature sparingly, as it can negate the benefit of two-factor authentication.'), + '$device_label' => DI::l10n()->t('Device'), + '$os_label' => DI::l10n()->t('OS'), + '$browser_label' => DI::l10n()->t('Browser'), + '$created_label' => DI::l10n()->t('Trusted'), + '$last_used_label' => DI::l10n()->t('Last Use'), + '$remove_label' => DI::l10n()->t('Remove'), + '$remove_all_label' => DI::l10n()->t('Remove All'), + + '$trusted_browsers' => $trustedBrowserDisplay, + ]); + } +} diff --git a/static/routes.config.php b/static/routes.config.php index 4ad122fbf2..1fd54aecae 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -385,6 +385,7 @@ return [ '/recovery' => [Module\Settings\TwoFactor\Recovery::class, [R::GET, R::POST]], '/app_specific' => [Module\Settings\TwoFactor\AppSpecific::class, [R::GET, R::POST]], '/verify' => [Module\Settings\TwoFactor\Verify::class, [R::GET, R::POST]], + '/trusted' => [Module\Settings\TwoFactor\Trusted::class, [R::GET, R::POST]], ], '/delegation[/{action}/{user_id}]' => [Module\Settings\Delegation::class, [R::GET, R::POST]], '/display' => [Module\Settings\Display::class, [R::GET, R::POST]], diff --git a/view/templates/settings/twofactor/index.tpl b/view/templates/settings/twofactor/index.tpl index 6cf3fac119..1f8f55b6cf 100644 --- a/view/templates/settings/twofactor/index.tpl +++ b/view/templates/settings/twofactor/index.tpl @@ -30,6 +30,7 @@ {{if $has_secret && $verified}}
+ {{/if}} {{if $has_secret && !$verified}} diff --git a/view/templates/settings/twofactor/trusted_browsers.tpl b/view/templates/settings/twofactor/trusted_browsers.tpl new file mode 100644 index 0000000000..e6d434b4fe --- /dev/null +++ b/view/templates/settings/twofactor/trusted_browsers.tpl @@ -0,0 +1,48 @@ +