diff --git a/src/App/Router.php b/src/App/Router.php index afea901cf6..0a500b8848 100644 --- a/src/App/Router.php +++ b/src/App/Router.php @@ -47,6 +47,10 @@ class Router $collector->addRoute(['GET'], '/webfinger' , Module\Xrd::class); $collector->addRoute(['GET'], '/x-social-relay' , Module\WellKnown\XSocialRelay::class); }); + $this->routeCollector->addGroup('/2fa', function (RouteCollector $collector) { + $collector->addRoute(['GET', 'POST'], '[/]' , Module\TwoFactor\Verify::class); + $collector->addRoute(['GET', 'POST'], '/recovery' , Module\TwoFactor\Recovery::class); + }); $this->routeCollector->addGroup('/admin', function (RouteCollector $collector) { $collector->addRoute(['GET'] , '[/]' , Module\Admin\Summary::class); diff --git a/src/Core/Authentication.php b/src/Core/Authentication.php index 1826602df4..bf7a9eb76f 100644 --- a/src/Core/Authentication.php +++ b/src/Core/Authentication.php @@ -5,6 +5,7 @@ namespace Friendica\Core; +use Friendica\App; use Friendica\BaseObject; use Friendica\Util\BaseURL; @@ -61,5 +62,26 @@ class Authentication extends BaseObject session_unset(); session_destroy(); } + + public static function twoFactorCheck($uid, App $a) + { + // Check user setting, if 2FA disabled return + if (!PConfig::get($uid, '2fa', 'verified')) { + return; + } + + // Check current path, if 2fa authentication module return + if ($a->argc > 0 && in_array($a->argv[0], ['ping', '2fa', 'view', 'help', 'logout'])) { + return; + } + + // Case 1: 2FA session present and valid: return + if (Session::get('2fa')) { + return; + } + + // Case 2: No valid 2FA session: redirect to code verification page + $a->internalRedirect('2fa'); + } } diff --git a/src/Core/Session.php b/src/Core/Session.php index 9dadbb1682..5ef77094ce 100644 --- a/src/Core/Session.php +++ b/src/Core/Session.php @@ -186,6 +186,8 @@ class Session } } + Authentication::twoFactorCheck($user_record['uid'], $a); + if ($interactive) { if ($user_record['login_date'] <= DBA::NULL_DATETIME) { info(L10n::t('Welcome %s', $user_record['username'])); diff --git a/src/Module/TwoFactor/Recovery.php b/src/Module/TwoFactor/Recovery.php new file mode 100644 index 0000000000..cd197e030f --- /dev/null +++ b/src/Module/TwoFactor/Recovery.php @@ -0,0 +1,71 @@ +user, true, true); + } else { + notice(L10n::t('Invalid code, please retry.')); + } + } + } + + public static function content() + { + if (!local_user()) { + self::getApp()->internalRedirect(); + } + + // Already authenticated with 2FA token + if (Session::get('2fa')) { + self::getApp()->internalRedirect(); + } + + return Renderer::replaceMacros(Renderer::getMarkupTemplate('twofactor/recovery.tpl'), [ + '$form_security_token' => self::getFormSecurityToken('twofactor_recovery'), + '$title' => L10n::t('Two-factor recovery'), + '$message' => L10n::t('

You can enter one of your one-time recovery codes in case you lost access to your mobile device.

'), + '$recovery_message' => L10n::t('Don’t have your phone? Enter a two-factor recovery code', '2fa/recovery'), + '$recovery_code' => ['recovery_code', L10n::t('Please enter a recovery code'), '', '', '', 'placeholder="000000-000000"'], + '$recovery_label' => L10n::t('Submit recovery code and complete login'), + ]); + } +} diff --git a/src/Module/TwoFactor/Verify.php b/src/Module/TwoFactor/Verify.php new file mode 100644 index 0000000000..b27c982bd8 --- /dev/null +++ b/src/Module/TwoFactor/Verify.php @@ -0,0 +1,66 @@ +verifyKey(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); + + // Resume normal login workflow + Session::setAuthenticatedForUser($a, $a->user, true, true); + } else { + notice(L10n::t('Invalid code, please retry.')); + } + } + } + + public static function content() + { + if (!local_user()) { + self::getApp()->internalRedirect(); + } + + // Already authenticated with 2FA token + if (Session::get('2fa')) { + self::getApp()->internalRedirect(); + } + + return Renderer::replaceMacros(Renderer::getMarkupTemplate('twofactor/verify.tpl'), [ + '$form_security_token' => self::getFormSecurityToken('twofactor_verify'), + '$title' => L10n::t('Two-factor authentication'), + '$message' => L10n::t('

Open the two-factor authentication app on your device to get an authentication code and verify your identity.

'), + '$recovery_message' => L10n::t('Don’t have your phone? Enter a two-factor recovery code', '2fa/recovery'), + '$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 complete login'), + ]); + } +} diff --git a/view/templates/twofactor/recovery.tpl b/view/templates/twofactor/recovery.tpl new file mode 100644 index 0000000000..c32c8d2d8b --- /dev/null +++ b/view/templates/twofactor/recovery.tpl @@ -0,0 +1,14 @@ +
+

{{$title}}

+
{{$message nofilter}}
+ +
+ + + {{include file="field_input.tpl" field=$recovery_code}} + +
+ +
+
+
diff --git a/view/templates/twofactor/verify.tpl b/view/templates/twofactor/verify.tpl new file mode 100644 index 0000000000..d75d6291a3 --- /dev/null +++ b/view/templates/twofactor/verify.tpl @@ -0,0 +1,15 @@ +
+

{{$title}}

+
{{$message nofilter}}
+ +
+ + + {{include file="field_input.tpl" field=$verify_code}} + +
+ +
+
+
{{$recovery_message nofilter}}
+