Add two-factor authentication
authorHypolite Petovan <hypolite@mrpetovan.com>
Mon, 13 May 2019 05:36:09 +0000 (01:36 -0400)
committerHypolite Petovan <hypolite@mrpetovan.com>
Mon, 13 May 2019 05:52:01 +0000 (01:52 -0400)
- Add 2FA login interception in Session::setAuthenticatedForUser
- Add 2fa session variable holding the last auth code

src/App/Router.php
src/Core/Authentication.php
src/Core/Session.php
src/Module/TwoFactor/Recovery.php [new file with mode: 0644]
src/Module/TwoFactor/Verify.php [new file with mode: 0644]
view/templates/twofactor/recovery.tpl [new file with mode: 0644]
view/templates/twofactor/verify.tpl [new file with mode: 0644]

index afea901..0a500b8 100644 (file)
@@ -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);
 
index 1826602..bf7a9eb 100644 (file)
@@ -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');
+       }
 }
 
index 9dadbb1..5ef7709 100644 (file)
@@ -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 (file)
index 0000000..cd197e0
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+
+namespace Friendica\Module\TwoFactor;
+
+use Friendica\BaseModule;
+use Friendica\Core\L10n;
+use Friendica\Core\Renderer;
+use Friendica\Core\Session;
+use Friendica\Model\TwoFactorRecoveryCode;
+
+/**
+ * // Page 1a: Recovery code verification
+ *
+ * @package Friendica\Module\TwoFactor
+ */
+class Recovery extends BaseModule
+{
+       public static function init()
+       {
+               if (!local_user()) {
+                       return;
+               }
+       }
+
+       public static function post()
+       {
+               if (!local_user()) {
+                       return;
+               }
+
+               if (defaults($_POST, 'action', null) == 'recover') {
+                       self::checkFormSecurityTokenRedirectOnError('2fa', 'twofactor_recovery');
+
+                       $a = self::getApp();
+
+                       $recovery_code = defaults($_POST, 'recovery_code', '');
+
+                       if (TwoFactorRecoveryCode::existsForUser(local_user(), $recovery_code)) {
+                               TwoFactorRecoveryCode::markUsedForUser(local_user(), $recovery_code);
+                               Session::set('2fa', true);
+                               notice(L10n::t('Remaining recovery codes: %d', TwoFactorRecoveryCode::countValidForUser(local_user())));
+
+                               // 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/recovery.tpl'), [
+                       '$form_security_token' => self::getFormSecurityToken('twofactor_recovery'),
+                       '$title' => L10n::t('Two-factor recovery'),
+                       '$message' => L10n::t('<p>You can enter one of your one-time recovery codes in case you lost access to your mobile device.</p>'),
+                       '$recovery_message' => L10n::t('Don’t have your phone? <a href="%s">Enter a two-factor recovery code</a>', '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 (file)
index 0000000..b27c982
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+namespace Friendica\Module\TwoFactor;
+
+use Friendica\BaseModule;
+use Friendica\Core\L10n;
+use Friendica\Core\PConfig;
+use Friendica\Core\Renderer;
+use Friendica\Core\Session;
+use PragmaRX\Google2FA\Google2FA;
+
+/**
+ * Page 1: Authenticator code verification
+ *
+ * @package Friendica\Module\TwoFactor
+ */
+class Verify extends BaseModule
+{
+       public static function post()
+       {
+               if (!local_user()) {
+                       return;
+               }
+
+               if (defaults($_POST, 'action', null) == 'verify') {
+                       self::checkFormSecurityTokenRedirectOnError('2fa', 'twofactor_verify');
+
+                       $a = self::getApp();
+
+                       $code = defaults($_POST, 'verify_code', '');
+
+                       $valid = (new Google2FA())->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('<p>Open the two-factor authentication app on your device to get an authentication code and verify your identity.</p>'),
+                       '$recovery_message' => L10n::t('Don’t have your phone? <a href="%s">Enter a two-factor recovery code</a>', '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 (file)
index 0000000..c32c8d2
--- /dev/null
@@ -0,0 +1,14 @@
+<div class="generic-page-wrapper">
+       <h1>{{$title}}</h1>
+       <div>{{$message nofilter}}</div>
+
+       <form action="" method="post">
+               <input type="hidden" name="form_security_token" value="{{$form_security_token}}">
+
+               {{include file="field_input.tpl" field=$recovery_code}}
+
+               <div class="form-group settings-submit-wrapper">
+                       <button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="recover">{{$recovery_label}}</button>
+               </div>
+       </form>
+</div>
diff --git a/view/templates/twofactor/verify.tpl b/view/templates/twofactor/verify.tpl
new file mode 100644 (file)
index 0000000..d75d629
--- /dev/null
@@ -0,0 +1,15 @@
+<div class="generic-page-wrapper">
+       <h1>{{$title}}</h1>
+       <div>{{$message nofilter}}</div>
+
+       <form action="" method="post">
+               <input type="hidden" name="form_security_token" value="{{$form_security_token}}">
+
+               {{include file="field_input.tpl" field=$verify_code}}
+
+               <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>
+       </form>
+       <div>{{$recovery_message nofilter}}</div>
+</div>