From 8e885f5b971320c1f86075f86ce085cf39c4bd13 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Mon, 13 May 2019 01:38:15 -0400 Subject: [PATCH] Add two-factor authentication settings - Add settings aside menu entry - Add two-factor authentication documentation --- doc/Two-Factor-Authentication.md | 60 ++++++++ mod/settings.php | 7 + src/App/Router.php | 8 ++ src/Module/BaseSettingsModule.php | 110 +++++++++++++++ src/Module/Settings/TwoFactor/Index.php | 111 +++++++++++++++ src/Module/Settings/TwoFactor/Recovery.php | 86 ++++++++++++ src/Module/Settings/TwoFactor/Verify.php | 129 ++++++++++++++++++ view/templates/settings/twofactor/index.tpl | 39 ++++++ .../templates/settings/twofactor/recovery.tpl | 28 ++++ view/templates/settings/twofactor/verify.tpl | 22 +++ 10 files changed, 600 insertions(+) create mode 100644 doc/Two-Factor-Authentication.md create mode 100644 src/Module/BaseSettingsModule.php create mode 100644 src/Module/Settings/TwoFactor/Index.php create mode 100644 src/Module/Settings/TwoFactor/Recovery.php create mode 100644 src/Module/Settings/TwoFactor/Verify.php create mode 100644 view/templates/settings/twofactor/index.tpl create mode 100644 view/templates/settings/twofactor/recovery.tpl create mode 100644 view/templates/settings/twofactor/verify.tpl diff --git a/doc/Two-Factor-Authentication.md b/doc/Two-Factor-Authentication.md new file mode 100644 index 0000000000..32aa7308a3 --- /dev/null +++ b/doc/Two-Factor-Authentication.md @@ -0,0 +1,60 @@ +# Configuring two-factor authentication + +* [Home](help) + +You can configure two-factor authentication using a mobile app. +A time-based one-time password (TOTP) application automatically generates an authentication code that changes after a certain period of time. + +**Tip**: To configure authentication via TOTP on multiple devices, during setup, scan the QR code using each device at the same time. +If 2FA is already enabled and you want to add another device, you must re-configure 2FA from your security settings. + +## Enabling two-factor authentication + +### 1. Download an authenticator app + +Any authenticator app should work with Friendica. +Notheless, we recommend: + + - For iOS, [Matt Rubin's MIT-licensed Authenticator app](https://mattrubin.me/authenticator). + - For Android, [andOTP](https://github.com/andOTP/andOTP). + +### 2. Record your one-use recovery codes + +From your [two-factor authentication user settings](/settings/2fa), enter your password and click on "Enable two-factor authentication". + +You will be presented with a list of one-use recovery codes. +Please save those in the same place you are saving your Friendica password (ideally, in a password manager like [KeePass](https://keepass.info)). + +When you're done, click on "Next". + +### 3. Setup your authenticator app + +You have three methods to setup your authenticator app: + +1. Scan the QR Code with your device camera. + This will automatically configure your account on the app. +2. Click/tap on the provided **totp://** URl. + Ideally your authenticator app should be called with this URL and set up your account. +3. Enter your account settings manually. + Friendica is using default settings for token type, code digit count and hashing algorithm but you may be required to enter them in your app. + +**Tip**: If you have multiple devices, configure them all at this point. + +Then verify your app is correctly configured by submitting a code provided by your app. +This will conclude two-factor authentication configuration. + +**Note:** If you leave this screen at any point without having submitted a verification code, two-factor authentication won't be enabled on your account. +To complete the configuration, just come back to your [two-factor authentication user settings](/settings/2fa) and click on "Finish configuration" after entering your current password. + +## Disabling two-factor authentication + +You can disable two-factor authentication at any time by going to your [two-factor authentication user settings](/settings/2fa) and click on "Disable two-factor authentication" after entering your current password. + +You should remove your Friendica account from your authenticator app as it won't work again even if you reenable two-factor authentication. +In this case you will have to configure your authenticator app again using the process above. + +## Managing your one-time recovery codes + +When two-factor authentication is enabled, you can show your recovery codes, including the ones you've already used. + +You can freely regenerate a new set of fresh recovery codes, just be sure to replace the previous ones where you saved them as they won't be active anymore. diff --git a/mod/settings.php b/mod/settings.php index 536c833542..45f11cdb6c 100644 --- a/mod/settings.php +++ b/mod/settings.php @@ -67,6 +67,13 @@ function settings_init(App $a) ], ]; + $tabs[] = [ + 'label' => L10n::t('Two-factor authentication'), + 'url' => 'settings/2fa', + 'selected' => (($a->argc > 1) && ($a->argv[1] === '2fa') ? 'active' : ''), + 'accesskey' => 'o', + ]; + $tabs[] = [ 'label' => L10n::t('Profiles'), 'url' => 'profiles', diff --git a/src/App/Router.php b/src/App/Router.php index 0a500b8848..cd59c3dd90 100644 --- a/src/App/Router.php +++ b/src/App/Router.php @@ -188,6 +188,14 @@ class Router $collector->addRoute(['GET'], '/{sub1}/{url}' , Module\Proxy::class); $collector->addRoute(['GET'], '/{sub1}/{sub2}/{url}' , Module\Proxy::class); }); + + $this->routeCollector->addGroup('/settings', function (RouteCollector $collector) { + $collector->addGroup('/2fa', function (RouteCollector $collector) { + $collector->addRoute(['GET', 'POST'], '[/]' , Module\Settings\TwoFactor\Index::class); + $collector->addRoute(['GET', 'POST'], '/recovery' , Module\Settings\TwoFactor\Recovery::class); + $collector->addRoute(['GET', 'POST'], '/verify' , Module\Settings\TwoFactor\Verify::class); + }); + }); $this->routeCollector->addRoute(['GET', 'POST'], '/register', Module\Register::class); $this->routeCollector->addRoute(['GET'], '/robots.txt', Module\RobotsTxt::class); $this->routeCollector->addRoute(['GET'], '/rsd.xml', Module\ReallySimpleDiscovery::class); diff --git a/src/Module/BaseSettingsModule.php b/src/Module/BaseSettingsModule.php new file mode 100644 index 0000000000..fdf3c81664 --- /dev/null +++ b/src/Module/BaseSettingsModule.php @@ -0,0 +1,110 @@ +page['htmlhead'] .= Renderer::replaceMacros($tpl, [ + '$ispublic' => L10n::t('everybody') + ]); + + $tabs = []; + + $tabs[] = [ + 'label' => L10n::t('Account'), + 'url' => 'settings', + 'selected' => (($a->argc == 1) && ($a->argv[0] === 'settings') ? 'active' : ''), + 'accesskey' => 'o', + ]; + + $tabs[] = [ + 'label' => L10n::t('Two-factor authentication'), + 'url' => 'settings/2fa', + 'selected' => (($a->argc > 1) && ($a->argv[1] === '2fa') ? 'active' : ''), + 'accesskey' => 'o', + ]; + + $tabs[] = [ + 'label' => L10n::t('Profiles'), + 'url' => 'profiles', + 'selected' => (($a->argc == 1) && ($a->argv[0] === 'profiles') ? 'active' : ''), + 'accesskey' => 'p', + ]; + + if (Feature::get()) { + $tabs[] = [ + 'label' => L10n::t('Additional features'), + 'url' => 'settings/features', + 'selected' => (($a->argc > 1) && ($a->argv[1] === 'features') ? 'active' : ''), + 'accesskey' => 't', + ]; + } + + $tabs[] = [ + 'label' => L10n::t('Display'), + 'url' => 'settings/display', + 'selected' => (($a->argc > 1) && ($a->argv[1] === 'display') ? 'active' : ''), + 'accesskey' => 'i', + ]; + + $tabs[] = [ + 'label' => L10n::t('Social Networks'), + 'url' => 'settings/connectors', + 'selected' => (($a->argc > 1) && ($a->argv[1] === 'connectors') ? 'active' : ''), + 'accesskey' => 'w', + ]; + + $tabs[] = [ + 'label' => L10n::t('Addons'), + 'url' => 'settings/addon', + 'selected' => (($a->argc > 1) && ($a->argv[1] === 'addon') ? 'active' : ''), + 'accesskey' => 'l', + ]; + + $tabs[] = [ + 'label' => L10n::t('Delegations'), + 'url' => 'delegate', + 'selected' => (($a->argc == 1) && ($a->argv[0] === 'delegate') ? 'active' : ''), + 'accesskey' => 'd', + ]; + + $tabs[] = [ + 'label' => L10n::t('Connected apps'), + 'url' => 'settings/oauth', + 'selected' => (($a->argc > 1) && ($a->argv[1] === 'oauth') ? 'active' : ''), + 'accesskey' => 'b', + ]; + + $tabs[] = [ + 'label' => L10n::t('Export personal data'), + 'url' => 'uexport', + 'selected' => (($a->argc == 1) && ($a->argv[0] === 'uexport') ? 'active' : ''), + 'accesskey' => 'e', + ]; + + $tabs[] = [ + 'label' => L10n::t('Remove account'), + 'url' => 'removeme', + 'selected' => (($a->argc == 1) && ($a->argv[0] === 'removeme') ? 'active' : ''), + 'accesskey' => 'r', + ]; + + + $tabtpl = Renderer::getMarkupTemplate("generic_links_widget.tpl"); + $a->page['aside'] = Renderer::replaceMacros($tabtpl, [ + '$title' => L10n::t('Settings'), + '$class' => 'settings-widget', + '$items' => $tabs, + ]); + } +} diff --git a/src/Module/Settings/TwoFactor/Index.php b/src/Module/Settings/TwoFactor/Index.php new file mode 100644 index 0000000000..7b80312720 --- /dev/null +++ b/src/Module/Settings/TwoFactor/Index.php @@ -0,0 +1,111 @@ +generateSecretKey(32)); + + self::getApp()->internalRedirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password')); + } + break; + case 'disable': + if ($has_secret) { + TwoFactorRecoveryCode::deleteForUser(local_user()); + PConfig::delete(local_user(), '2fa', 'secret'); + PConfig::delete(local_user(), '2fa', 'verified'); + Session::remove('2fa'); + + notice(L10n::t('Two-factor authentication successfully disabled.')); + self::getApp()->internalRedirect('settings/2fa'); + } + break; + case 'recovery': + if ($has_secret) { + self::getApp()->internalRedirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password')); + } + break; + case 'configure': + if (!$verified) { + self::getApp()->internalRedirect('settings/2fa/verify?t=' . self::getFormSecurityToken('settings_2fa_password')); + } + break; + } + } catch (\Exception $e) { + notice(L10n::t('Wrong Password')); + } + } + + public static function content() + { + if (!local_user()) { + return Login::form('settings/2fa'); + } + + parent::content(); + + $has_secret = (bool) PConfig::get(local_user(), '2fa', 'secret'); + $verified = PConfig::get(local_user(), '2fa', 'verified'); + + return Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/twofactor/index.tpl'), [ + '$form_security_token' => self::getFormSecurityToken('settings_2fa'), + '$title' => L10n::t('Two-factor authentication'), + '$help_label' => L10n::t('Help'), + + '$status_title' => L10n::t('Status'), + '$message' => L10n::t('

Use an application on a mobile device to get two-factor authentication codes when prompted on login.

'), + + '$has_secret' => $has_secret, + '$verified' => $verified, + + '$auth_app_label' => L10n::t('Authenticator app'), + '$app_status' => $has_secret ? $verified ? L10n::t('Configured') : L10n::t('Not Configured') : L10n::t('Disabled'), + '$not_configured_message' => L10n::t('

You haven\'t finished configuring your authenticator app.

'), + '$configured_message' => L10n::t('

Your authenticator app is correctly configured.

'), + + '$recovery_codes_title' => L10n::t('Recovery codes'), + '$recovery_codes_remaining' => L10n::t('Remaining valid codes'), + '$recovery_codes_count' => TwoFactorRecoveryCode::countValidForUser(local_user()), + '$recovery_codes_message' => L10n::t('

These one-use codes can replace an authenticator app code in case you have lost access to it.

'), + + '$action_title' => L10n::t('Actions'), + '$password' => ['password', L10n::t('Current password:'), '', L10n::t('You need to provide your current password to change two-factor authentication settings.'), 'required', 'autofocus'], + + '$enable_label' => L10n::t('Enable two-factor authentication'), + '$disable_label' => L10n::t('Disable two-factor authentication'), + '$recovery_codes_label' => L10n::t('Show recovery codes'), + '$configure_label' => L10n::t('Finish app configuration'), + ]); + } +} diff --git a/src/Module/Settings/TwoFactor/Recovery.php b/src/Module/Settings/TwoFactor/Recovery.php new file mode 100644 index 0000000000..9f0e74832e --- /dev/null +++ b/src/Module/Settings/TwoFactor/Recovery.php @@ -0,0 +1,86 @@ +internalRedirect('settings/2fa'); + } + + if (!self::checkFormSecurityToken('settings_2fa_password', 't')) { + notice(L10n::t('Please enter your password to access this page.')); + self::getApp()->internalRedirect('settings/2fa'); + } + } + + public static function post() + { + if (!local_user()) { + return; + } + + if (!empty($_POST['action'])) { + self::checkFormSecurityTokenRedirectOnError('settings/2fa/recovery', 'settings_2fa_recovery'); + + if ($_POST['action'] == 'regenerate') { + TwoFactorRecoveryCode::regenerateForUser(local_user()); + notice(L10n::t('New recovery codes successfully generated.')); + self::getApp()->internalRedirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password')); + } + } + } + + public static function content() + { + if (!local_user()) { + return Login::form('settings/2fa/recovery'); + } + + parent::content(); + + if (!TwoFactorRecoveryCode::countValidForUser(local_user())) { + TwoFactorRecoveryCode::generateForUser(local_user()); + } + + $recoveryCodes = TwoFactorRecoveryCode::getListForUser(local_user()); + + $verified = PConfig::get(local_user(), '2fa', 'verified'); + + return Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/twofactor/recovery.tpl'), [ + '$form_security_token' => self::getFormSecurityToken('settings_2fa_recovery'), + '$password_security_token' => self::getFormSecurityToken('settings_2fa_password'), + '$title' => L10n::t('Two-factor recovery codes'), + '$help_label' => L10n::t('Help'), + '$message' => L10n::t('

Recovery codes can be used to access your account in the event you lose access to your device and cannot receive two-factor authentication codes.

Put these in a safe spot! If you lose your device and don’t have the recovery codes you will lose access to your account.

'), + '$recovery_codes' => $recoveryCodes, + '$password' => ['password', L10n::t('Please enter your password for verification:'), '', L10n::t('You need to provide your current password to enable or disable two-factor authentication.'), 'required', 'autofocus'], + '$regenerate_message' => L10n::t('When you generate new recovery codes, you must copy the new codes. Your old codes won’t work anymore.'), + '$regenerate_label' => L10n::t('Generate new recovery codes'), + '$verified' => $verified, + '$verify_label' => L10n::t('Next: Verification'), + ]); + } +} diff --git a/src/Module/Settings/TwoFactor/Verify.php b/src/Module/Settings/TwoFactor/Verify.php new file mode 100644 index 0000000000..57995cd75c --- /dev/null +++ b/src/Module/Settings/TwoFactor/Verify.php @@ -0,0 +1,129 @@ +internalRedirect('settings/2fa'); + } + + if (!self::checkFormSecurityToken('settings_2fa_password', 't')) { + notice(L10n::t('Please enter your password to access this page.')); + self::getApp()->internalRedirect('settings/2fa'); + } + } + + public static function post() + { + if (!local_user()) { + return; + } + + if (defaults($_POST, 'action', null) == 'verify') { + self::checkFormSecurityTokenRedirectOnError('settings/2fa/verify', 'settings_2fa_verify'); + + $google2fa = new Google2FA(); + + $valid = $google2fa->verifyKey(PConfig::get(local_user(), '2fa', 'secret'), defaults($_POST, 'verify_code', '')); + + if ($valid) { + PConfig::set(local_user(), '2fa', 'verified', true); + Session::set('2fa', true); + + notice(L10n::t('Two-factor authentication successfully activated.')); + + self::getApp()->internalRedirect('settings/2fa'); + } else { + notice(L10n::t('Invalid code, please retry.')); + } + } + } + + public static function content() + { + if (!local_user()) { + return Login::form('settings/2fa/verify'); + } + + parent::content(); + + $company = 'Friendica'; + $holder = Session::get('my_address'); + $secret = PConfig::get(local_user(), '2fa', 'secret'); + + $otpauthUrl = (new Google2FA())->getQRCodeUrl($company, $holder, $secret); + + $renderer = (new \BaconQrCode\Renderer\Image\Svg()) + ->setHeight(256) + ->setWidth(256); + + $writer = new Writer($renderer); + + $qrcode_image = str_replace('', '', $writer->writeString($otpauthUrl)); + + $shortOtpauthUrl = explode('?', $otpauthUrl)[0]; + + $manual_message = L10n::t('

Or you can submit the authentication settings manually:

+
+
Issuer
+
%s
+
Account Name
+
%s
+
Secret Key
+
%s
+
Type
+
Time-based
+
Number of digits
+
6
+
Hashing algorithm
+
SHA-1
+
', $company, $holder, $secret); + + return Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/twofactor/verify.tpl'), [ + '$form_security_token' => self::getFormSecurityToken('settings_2fa_verify'), + '$password_security_token' => self::getFormSecurityToken('settings_2fa_password'), + '$title' => L10n::t('Two-factor code verification'), + '$help_label' => L10n::t('Help'), + '$message' => L10n::t('

Please scan this QR Code with your authenticator app and submit the provided code.

'), + '$qrcode_image' => $qrcode_image, + '$qrcode_url_message' => L10n::t('

Or you can open the following URL in your mobile devicde:

%s

', $otpauthUrl, $shortOtpauthUrl), + '$manual_message' => $manual_message, + '$company' => $company, + '$holder' => $holder, + '$secret' => $secret, + + '$verify_code' => ['verify_code', L10n::t('Please enter a code from your authentication app'), '', '', 'required', 'autofocus placeholder="000000"'], + '$verify_label' => L10n::t('Verify code and enable two-factor authentication'), + ]); + } +} diff --git a/view/templates/settings/twofactor/index.tpl b/view/templates/settings/twofactor/index.tpl new file mode 100644 index 0000000000..57bcab19ea --- /dev/null +++ b/view/templates/settings/twofactor/index.tpl @@ -0,0 +1,39 @@ +
+

{{$title}}

+
{{$message nofilter}}
+

{{$status_title}}

+

{{$auth_app_label}}: {{$app_status}}

+{{if $has_secret && $verified}} +
{{$configured_message nofilter}}
+{{/if}} +{{if $has_secret && !$verified}} +
{{$not_configured_message nofilter}}
+{{/if}} + +{{if $has_secret && $verified}} +

{{$recovery_codes_title}}

+

{{$recovery_codes_remaining}}: {{$recovery_codes_count}}

+
{{$recovery_codes_message nofilter}}
+{{/if}} + +
+

{{$action_title}}

+ + + {{include file="field_password.tpl" field=$password}} + +
+{{if !$has_secret}} + +{{else}} + +{{/if}} +{{if $has_secret && $verified}} + +{{/if}} +{{if $has_secret && !$verified}} + +{{/if}} +
+
+
diff --git a/view/templates/settings/twofactor/recovery.tpl b/view/templates/settings/twofactor/recovery.tpl new file mode 100644 index 0000000000..8ecd0198d9 --- /dev/null +++ b/view/templates/settings/twofactor/recovery.tpl @@ -0,0 +1,28 @@ +
+

{{$title}}

+
{{$message nofilter}}
+ + + +{{if $verified}} +
+

{{$regenerate_label}}

+ +
{{$regenerate_message}}
+ +
+ +
+
+{{else}} +

{{$verify_label}}

+{{/if}} +
diff --git a/view/templates/settings/twofactor/verify.tpl b/view/templates/settings/twofactor/verify.tpl new file mode 100644 index 0000000000..03138659e2 --- /dev/null +++ b/view/templates/settings/twofactor/verify.tpl @@ -0,0 +1,22 @@ +
+

{{$title}}

+
{{$message nofilter}}
+ +
+ {{$qrcode_image nofilter}} +
+ +
+ + + {{include file="field_input.tpl" field=$verify_code}} + +
+ +
+
+ +
{{$qrcode_url_message nofilter}}
+ +
{{$manual_message nofilter}}
+