0013346: client presence observer for pin validation
authorPhilipp Schüle <p.schuele@metaways.de>
Wed, 19 Jul 2017 14:16:06 +0000 (16:16 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Thu, 20 Jul 2017 13:47:46 +0000 (15:47 +0200)
* use presenceObserver to detect user absence/presence
* default lifetime is 15 minutes
* don't allow empty pins

https://forge.tine20.org/view.php?id=13346

Change-Id: Icccb593a947d6a6e0979c9935199eb9c3e10f183
Reviewed-on: http://gerrit.tine20.com/customers/5259
Tested-by: Jenkins CI (http://ci.tine20.com/)
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
tests/tine20/Tinebase/Auth/SecondFactor/Mock.php
tests/tine20/Tinebase/AuthTest.php
tine20/Tinebase/Auth.php
tine20/Tinebase/Auth/SecondFactor/Abstract.php
tine20/Tinebase/Auth/SecondFactor/PrivacyIdea.php
tine20/Tinebase/Auth/SecondFactor/Tine20.php
tine20/Tinebase/Config.php
tine20/Tinebase/Controller.php
tine20/Tinebase/Frontend/Json.php
tine20/Tinebase/js/widgets/MainScreen.js
tine20/Tinebase/js/widgets/dialog/SecondFactorDialog.js

index ced14a2..afc5aa5 100644 (file)
@@ -10,7 +10,7 @@
  */
 class Tinebase_Auth_SecondFactor_Mock extends Tinebase_Auth_SecondFactor_Abstract
 {
-    public function validate($username, $password)
+    public function validate($username, $password, $allowEmpty = false)
     {
         return Tinebase_Auth::SUCCESS;
     }
index 629f0b1..c94a750 100644 (file)
@@ -229,6 +229,12 @@ class Tinebase_AuthTest extends TestCase
     public function testSecondFactorTine20()
     {
         $user = Tinebase_Core::getUser();
+        $result = Tinebase_Auth::validateSecondFactor($user->accountLoginName, '', array(
+            'active' => true,
+            'provider' => 'Tine20',
+        ));
+        $this->assertEquals(Tinebase_Auth::FAILURE, $result, 'empty password should always fail');
+
         Tinebase_User::getInstance()->setPin($user, '1234');
         $result = Tinebase_Auth::validateSecondFactor($user->accountLoginName, '1234', array(
             'active' => true,
index 422c0c9..d732966 100755 (executable)
@@ -441,10 +441,11 @@ class Tinebase_Auth
      * @param string $username
      * @param string $password
      * @param array $options
+     * @param boolean $allowEmpty
      * @return int
      * @throws Tinebase_Exception_Backend
      */
-    public static function validateSecondFactor($username, $password, $options = null)
+    public static function validateSecondFactor($username, $password, $options = null, $allowEmpty = false)
     {
         if (! $options) {
             $options = Tinebase_Config::getInstance()->get(
@@ -457,7 +458,7 @@ class Tinebase_Auth
             $authProviderClass = 'Tinebase_Auth_SecondFactor_' . $options['provider'];
             if (class_exists($authProviderClass)) {
                 $authProvider = new $authProviderClass($options);
-                return $authProvider->validate($username, $password);
+                return $authProvider->validate($username, $password, $allowEmpty);
             }
         }
         throw new Tinebase_Exception_Backend('Second factor backend not recognized / misconfigured');
index 9c349a2..2060de7 100644 (file)
@@ -20,19 +20,28 @@ abstract class Tinebase_Auth_SecondFactor_Abstract
     /**
      * validate second factor
      *
-     * @param $username
-     * @param $password
-     * @return mixed
+     * @param string $username
+     * @param string $password
+     * @param boolean $allowEmpty
+     * @return integer (Tinebase_Auth::FAILURE|Tinebase_Auth::SUCCESS)
      */
-    abstract public function validate($username, $password);
+    abstract public function validate($username, $password, $allowEmpty = false);
 
     /**
-     * @param int $lifetimeMinutes
+     * @param int|null $lifetimeMinutes
      * @throws Exception
-     * @throws Zend_Session_Exception
+     * @throws Zend_Session_Exception,
      */
-    public static function saveValidSecondFactor($lifetimeMinutes = 15)
+    public static function saveValidSecondFactor($lifetimeMinutes = null)
     {
+        if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
+            . ' saveValidSecondFactor for ' . $lifetimeMinutes);
+
+        if ($lifetimeMinutes === null) {
+            $sfConfig = Tinebase_Config::getInstance()->get(Tinebase_Config::AUTHENTICATIONSECONDFACTOR);
+            $lifetimeMinutes = $sfConfig->sessionLifetime ? $sfConfig->sessionLifetime : 15;
+        }
+
         Tinebase_Session::getSessionNamespace()->secondFactorValidUntil =
             Tinebase_DateTime::now()->addMinute($lifetimeMinutes)->toString();
     }
index 9be0d37..8f9914b 100644 (file)
  */
 class Tinebase_Auth_SecondFactor_PrivacyIdea extends Tinebase_Auth_SecondFactor_Abstract
 {
-    public function validate($username, $password)
+    /**
+     * validate second factor
+     *
+     * @param string $username
+     * @param string $password
+     * @param boolean $allowEmpty
+     * @return integer (Tinebase_Auth::FAILURE|Tinebase_Auth::SUCCESS)
+     */
+    public function validate($username, $password, $allowEmpty = false)
     {
+        if (! $allowEmpty && empty($password)) {
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
+                __METHOD__ . '::' . __LINE__ . ' Empty password given');
+            return Tinebase_Auth::FAILURE;
+        }
+
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
             __METHOD__ . '::' . __LINE__ . ' Options: ' . print_r($this->_options, true));
 
index 9a219bc..3c8d218 100644 (file)
 class Tinebase_Auth_SecondFactor_Tine20 extends Tinebase_Auth_SecondFactor_Abstract
 {
     /**
-     * @param $username
-     * @param $password
-     * @return Zend_Auth_Result
+     * validate second factor
+     *
+     * @param string $username
+     * @param string $password
+     * @param boolean $allowEmpty
+     * @return integer (Tinebase_Auth::FAILURE|Tinebase_Auth::SUCCESS)
      */
-    public function validate($username, $password)
+    public function validate($username, $password, $allowEmpty = false)
     {
+        if (! $allowEmpty && empty($password)) {
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
+                __METHOD__ . '::' . __LINE__ . ' Empty password given');
+            return Tinebase_Auth::FAILURE;
+        }
+
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
             __METHOD__ . '::' . __LINE__ . ' Options: ' . print_r($this->_options, true));
 
index da10211..5a8c589 100644 (file)
@@ -705,6 +705,7 @@ class Tinebase_Config extends Tinebase_Config_Abstract
          *      'allow_self_signed'     => true,
          *      'ignorePeerName'        => true,
          *      'login'                 => true, // validate during login + show field on login screen
+         *      'sessionLifetime'       => 15 // minutes
          * )
          */
         self::AUTHENTICATIONSECONDFACTOR => array(
index e87e25b..18a5acc 100644 (file)
@@ -436,10 +436,15 @@ class Tinebase_Controller extends Tinebase_Controller_Event
             }
             Tinebase_User::getInstance()->setPassword($user, $_newPassword, true, false);
         } else {
-            $validateOldPin = Tinebase_Auth::validateSecondFactor($loginName, $_oldPassword, array(
-                'active' => true,
-                'provider' => 'Tine20',
-            ));
+            $validateOldPin = Tinebase_Auth::validateSecondFactor(
+                $loginName,
+                $_oldPassword,
+                array(
+                    'active' => true,
+                    'provider' => 'Tine20',
+                ),
+                /* $allowEmpty */ true
+            );
             if ($validateOldPin !== Tinebase_Auth::SUCCESS) {
                 throw new Tinebase_Exception_InvalidArgument('Old pin is wrong.');
             }
index 9ab0d25..563e58b 100644 (file)
@@ -784,9 +784,16 @@ class Tinebase_Frontend_Json extends Tinebase_Frontend_Json_Abstract
         
         $registryData =  array(
             'modSsl'           => Tinebase_Auth::getConfiguredBackend() == Tinebase_Auth::MODSSL,
+
+            // secondfactor config
+            // TODO pass sf config as array (but don't send everything to client)
             'secondFactor'     => $secondFactorConfig && $secondFactorConfig->active && $secondFactorConfig->login,
+            'secondFactorSessionLifetime'  => $secondFactorConfig && $secondFactorConfig->sessionLifetime
+                ? $secondFactorConfig->sessionLifetime
+                : 15,
             'secondFactorPinChangeAllowed' => $secondFactorConfig
                 && $secondFactorConfig->active && $secondFactorConfig->provider && $secondFactorConfig->provider === 'Tine20',
+
             'serviceMap'       => $tbFrontendHttp->getServiceMap(),
             'locale'           => array(
                 'locale'   => $locale->toString(),
@@ -1508,4 +1515,21 @@ class Tinebase_Frontend_Json extends Tinebase_Frontend_Json_Abstract
 
         return $result;
     }
+
+    /**
+     * @param string $lastPresence
+     */
+    public function reportPresence($lastPresence)
+    {
+        if (Tinebase_Auth_SecondFactor_Abstract::hasValidSecondFactor()) {
+            Tinebase_Auth_SecondFactor_Abstract::saveValidSecondFactor();
+            $result = true;
+        } else {
+            $result = false;
+        }
+
+        return array(
+            'success' => $result
+        );
+    }
 }
index a7a31c7..eb0e12b 100644 (file)
@@ -74,11 +74,20 @@ Tine.widgets.MainScreen = Ext.extend(Ext.Panel, {
         if (! this.rendered) {
             return;
         }
+
+        // get grid for removing or reloading data from grid depending on valid second factor
+        var cp = this.getCenterPanel(),
+            grid = cp ? cp.getGrid() : null;
+
         switch (e.topic) {
             case 'secondfactor.invalid':
-                // recycle old layer + dialog
+
+                this.getEl().mask();
+                if (grid) {
+                    grid.getStore().removeAll();
+                }
+
                 if (! this.secondfactorDialogLayer) {
-                    this.getEl().mask();
 
                     this.secondfactorDialog = new Tine.Tinebase.widgets.dialog.SecondFactorDialog();
                     this.secondfactorDialogLayer = new Ext.Layer({
@@ -101,17 +110,23 @@ Tine.widgets.MainScreen = Ext.extend(Ext.Panel, {
                     var height = 120;
                     this.innerLayer.dom.style.height = '';
                     this.innerLayer.setHeight(height);
-
-                    this.secondfactorDialogLayer.beginUpdate();
-                    this.secondfactorDialogLayer.setHeight(height);
-                    this.secondfactorDialogLayer.alignTo(this.getEl(), 'c', [-100, -50]);
-                    this.secondfactorDialogLayer.endUpdate();
                 }
+
+                // align layer
+                this.secondfactorDialogLayer.beginUpdate();
+                this.secondfactorDialogLayer.setHeight(height);
+                this.secondfactorDialogLayer.alignTo(this.getEl(), 'c', [-100, -50]);
+                this.secondfactorDialogLayer.endUpdate();
+
                 this.secondfactorDialogLayer.show();
 
                 break;
             case 'secondfactor.valid':
                 this.getEl().unmask();
+                if (grid) {
+                    grid.getStore().reload();
+                }
+
                 this.secondfactorDialogLayer.hide();
                 break;
         }
index 7c7e779..64740b8 100644 (file)
@@ -46,6 +46,27 @@ Tine.Tinebase.widgets.dialog.SecondFactorDialog = Ext.extend(Tine.Tinebase.widge
                             channel: "messagebus",
                             topic: 'secondfactor.valid'
                         });
+
+                        if (! Tine.Tinebase.widgets.dialog.SecondFactorDialog.presenceObserver) {
+                            var secondFactorLifetime = Tine.Tinebase.registry.get('secondFactorSessionLifetime') || 15;
+                            Tine.Tinebase.widgets.dialog.SecondFactorDialog.presenceObserver = new Tine.Tinebase.PresenceObserver({
+                                maxAbsenseTime: secondFactorLifetime / 3, // ping server each secondFactorLifetime / 3 minutes
+                                absenceCallback: function (lastPresence, po) {
+                                    window.postal.publish({
+                                        channel: "messagebus",
+                                        topic: 'secondfactor.invalid'
+                                    });
+                                },
+                                presenceCallback: function (lastPresence) {
+                                    // report presence to server
+                                    Tine.Tinebase.reportPresence(Ext.encode(lastPresence));
+                                }
+                            });
+
+                        } else {
+                            Tine.Tinebase.widgets.dialog.SecondFactorDialog.presenceObserver.startChecking();
+                        }
+
                     } else {
                         window.postal.publish({
                             channel: "messagebus",