From 927f7d024ac0a0e974d5620faa3fb539806cf889 Mon Sep 17 00:00:00 2001 From: Menno Dekker Date: Thu, 28 Mar 2013 10:08:25 +0100 Subject: [PATCH 1/9] initial check-in of the new auth framework, still need to add session handling to identity + change the two entry points: remote control and authentication action --- application/core/LSAuthResult.php | 36 ++++++++++ application/core/LSUserIdentity.php | 83 ++++++++++++++++++++++ application/core/plugins/Authdb/Authdb.php | 50 +++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 application/core/LSAuthResult.php create mode 100644 application/core/LSUserIdentity.php create mode 100644 application/core/plugins/Authdb/Authdb.php diff --git a/application/core/LSAuthResult.php b/application/core/LSAuthResult.php new file mode 100644 index 00000000000..d76b35a1fc5 --- /dev/null +++ b/application/core/LSAuthResult.php @@ -0,0 +1,36 @@ +setError($code, $message); + } + + public function isValid() + { + if ($this->_code === 0) { + return true; + } + + return false; + } + + public function setError($code, $message = null) { + $this->_code = $code; + $this->_message = $message; + } +} \ No newline at end of file diff --git a/application/core/LSUserIdentity.php b/application/core/LSUserIdentity.php new file mode 100644 index 00000000000..176d3291061 --- /dev/null +++ b/application/core/LSUserIdentity.php @@ -0,0 +1,83 @@ +_result = new LSAuthResult(); + + // Check if the ip is locked out + if (Failed_login_attempts::model()->isLockedOut()) { + $this->_result->setError(self::ERROR_IP_LOCKED_OUT); + } + + // If still ok, continue + if ($this->_result->isValid()) + { + if (is_null($this->plugin)) { + $this->_result->setError(self::ERROR_UNKNOWN_HANDLER); + } else { + // Delegate actual authentication to plugin + $authEvent = new PluginEvent('newSession', $this); + App()->getPluginManager()->dispatchEvent($authEvent, array($this->plugin)); + $result = $authEvent->get('result'); + if ($result instanceof LSAuthResult) { + + } + } + } + + @session_regenerate_id(); // Prevent session fixation + if ($this->_result->isValid()) { + // Perform postlogin + $this->postLogin(); + + } else { + // Log a failed attempt + $userHostAddress = App()->request->getUserHostAddress(); + Failed_login_attempts::model()->addAttempt($userHostAddress); + } + + return $this->_result->isValid(); + } + + public function setPlugin($name) { + $this->plugin = $name; + } +} \ No newline at end of file diff --git a/application/core/plugins/Authdb/Authdb.php b/application/core/plugins/Authdb/Authdb.php new file mode 100644 index 00000000000..6ef7b889baa --- /dev/null +++ b/application/core/plugins/Authdb/Authdb.php @@ -0,0 +1,50 @@ +subscribe('beforeLogin'); + $this->subscribe('afterCreateLoginForm'); + } + + public function beforeLogin(PluginEvent $event) + { + // We can skip the login form here and set username/password etc. + + /* @var $identity UserIdentity */ + $identity = $event->get('identity'); + + if (App()->getRequest()->isPostRequest() && !is_null(Yii::app()->request->getQuery('onepass'))) { + // We have a one time password, skip the login form + $identity->onepass = Yii::app()->getRequest()->getQuery('onepass'); + $identity->username = Yii::app()->getRequest()->getQuery('user'); + $event->stop(); // Skip the login form + } + } + + public function afterCreateLoginForm(PluginEvent $event) + { + // Here we can influence the way the login form looks + $blocks = $event->get('blocks', array()); + + $blocks['Authdb'] = ''; + $event->set('blocks', $blocks); + } + + public function afterLoginPost(PluginEvent $event) + { + // Here we handle the authentication, using the posted form data + $blocks = $event->get('blocks', array()); + $event->set('blocks', $blocks); + } + + +} \ No newline at end of file From 13cbada390d89264161a3cb49202287a3442feb4 Mon Sep 17 00:00:00 2001 From: Menno Dekker Date: Thu, 28 Mar 2013 15:56:22 +0100 Subject: [PATCH 2/9] dev: first working draft of the authdb plugin, should be enabled by default and does not handle forgot password yet --- application/controllers/AdminController.php | 2 +- .../controllers/admin/authentication.php | 213 ++++-------------- application/core/LSAuthResult.php | 12 +- application/core/LSUserIdentity.php | 108 +++++++-- application/core/plugins/Authdb/Authdb.php | 92 ++++++-- .../views/admin/authentication/login.php | 26 ++- 6 files changed, 251 insertions(+), 202 deletions(-) diff --git a/application/controllers/AdminController.php b/application/controllers/AdminController.php index 925c1ffdacb..0b706974729 100644 --- a/application/controllers/AdminController.php +++ b/application/controllers/AdminController.php @@ -149,7 +149,7 @@ public function run($action) if (!empty($action) && $action != 'index') Yii::app()->session['redirect_after_login'] = $this->createUrl('/'); - Yii::app()->session['redirectopage'] = Yii::app()->request->requestUri; + App()->user->setReturnUrl(App()->request->requestUri); $this->redirect(array('/admin/authentication/sa/login')); } diff --git a/application/controllers/admin/authentication.php b/application/controllers/admin/authentication.php index c77d6469ad8..54380c247ee 100644 --- a/application/controllers/admin/authentication.php +++ b/application/controllers/admin/authentication.php @@ -32,39 +32,53 @@ class Authentication extends Survey_Common_Action public function index() { $this->_redirectIfLoggedIn(); - $bCanLogin = $this->_userCanLogin(); - if ($bCanLogin && !is_array($bCanLogin)) - { - if (Yii::app()->request->getPost('action') || !is_null(Yii::app()->request->getQuery('onepass')) || Yii::app()->getConfig('auth_webserver') === true) - { + $beforeLogin = new PluginEvent('beforeLogin'); + $beforeLogin->set('identity', new LSUserIdentity('', '')); - $aData = $this->_doLogin(Yii::app()->request->getParam('user'), Yii::app()->request->getPost('password'),Yii::app()->request->getQuery('onepass','')); + App()->getPluginManager()->dispatchEvent($beforeLogin); + /* @var $identity LSUserIdentity */ + $identity = $beforeLogin->get('identity'); - if (!isset($aData['errormsg'])) - { - Failed_login_attempts::model()->deleteAttempts(); + if (!$beforeLogin->isStopped() && is_null(App()->getRequest()->getPost('login_submit'))) + { + $newLoginForm = new PluginEvent('newLoginForm'); + App()->getPluginManager()->dispatchEvent($newLoginForm); + $aData['summary'] = $this->_getSummary('logout'); + $aData['pluginContent'] = $newLoginForm->getAllContent(); + $this->_renderWrappedTemplate('authentication', 'login', $aData); + } else { + // Handle getting the post and populating the identity there + $authMethod = App()->getRequest()->getPost('authMethod'); + $identity->plugin = $authMethod; - $this->getController()->_GetSessionUserRights(Yii::app()->session['loginID']); - Yii::app()->session['just_logged_in'] = true; - Yii::app()->session['loginsummary'] = $this->_getSummary(); - $this->_doRedirect(); - die(); - } - else - { - $this->_renderWrappedTemplate('authentication', 'error', $aData); - } - } - else + $event = new PluginEvent('afterLoginFormSubmit'); + $event->set('identity', $identity); + App()->getPluginManager()->dispatchEvent($event, array($authMethod)); + $identity = $event->get('identity'); + + // Now authenticate + if ($identity->authenticate()) { - $this->_showLoginForm(); + Failed_login_attempts::model()->deleteAttempts(); + + $this->getController()->_GetSessionUserRights(Yii::app()->session['loginID']); + Yii::app()->session['just_logged_in'] = true; + Yii::app()->session['loginsummary'] = $this->_getSummary(); + $this->_doRedirect(); + + } else { + // Failed + $message = $identity->errorMessage; + if (empty($message)) { + // If no message, return a default message + $clang = $this->getController()->lang; + $message = $clang->gT('Incorrect username and/or password!'); + } + App()->user->setFlash('loginError', $message); + $this->getController()->redirect(array('/admin/authentication/sa/login')); } } - else - { - $this->_renderWrappedTemplate('authentication', 'error', $bCanLogin); - } } /** @@ -72,8 +86,9 @@ public function index() */ public function logout() { - Yii::app()->user->logout(); - $this->_showLoginForm($this->getController()->lang->gT('Logout successful.')); + App()->user->logout(); + App()->user->setFlash('loginmessage', gT('Logout successful.')); + $this->getController()->redirect(array('/admin/authentication/sa/login')); } /** @@ -149,16 +164,6 @@ private function _sendPasswordEmail($sEmailAddr, $aFields) return $sMessage; } - /** - * Show login screen - * @param optional message - */ - protected function _showLoginForm($sLogoutSummary = '') - { - $aData['summary'] = $this->_getSummary('logout', $sLogoutSummary); - $this->_renderWrappedTemplate('authentication', 'login', $aData); - } - /** * Get's the summary * @param string $sMethod login|logout @@ -230,136 +235,8 @@ private function _userCanLogin() */ private function _doRedirect() { - if (strlen(Yii::app()->session['redirectopage']) > 1) - { - $this->getController()->redirect(Yii::app()->session['redirectopage']); - } - else - { - $this->getController()->redirect(array('/admin')); - } - } - - /** - * Do the actual login work - * Note: This function is replicated in parts in remotecontrol.php controller - if you change this don't forget to make according changes there, too (which is why we should make a login helper) - * @param string $sUsername The username to login with - * @param string $sPassword The password to login with - * @return Array of data containing errors for the view - */ - private function _doLogin($sUsername, $sPassword, $sOneTimePassword) - { - $identity = new UserIdentity(sanitize_user($sUsername), $sPassword); - - if (!$identity->authenticate($sOneTimePassword)) - { - return $this->_getAuthenticationFailedErrorMessage(); - } - @session_regenerate_id(); // Prevent session fixation - return $this->_setLoginSessions($identity); - } - - /** - * Sets the login sessions - * @param UserIdentity $identity - * @return bool True - */ - private function _setLoginSessions($identity) - { - $user = $identity->getUser(); - - Yii::app()->user->login($identity); - $this->_checkForUsageOfDefaultPassword(); - $this->_setSessionData($user); - $this->_setLanguageSettings($user); - - return true; - } - - /** - * Sets the session data - * @param CActiveRecord $user - */ - private function _setSessionData($user) - { - Yii::app()->session['loginID'] = (int) $user->uid; - Yii::app()->session['user'] = $user->users_name; - Yii::app()->session['full_name'] = $user->full_name; - Yii::app()->session['htmleditormode'] = $user->htmleditormode; - Yii::app()->session['templateeditormode'] = $user->templateeditormode; - Yii::app()->session['questionselectormode'] = $user->questionselectormode; - Yii::app()->session['dateformat'] = $user->dateformat; - Yii::app()->session['session_hash'] = hash('sha256',getGlobalSetting('SessionName').$user->users_name.$user->uid); - } - - /** - * Sets the language settings for the user - * @param CActiveRecord $user - */ - private function _setLanguageSettings($user) - { - if (Yii::app()->request->getPost('loginlang','default') != 'default') - { - $user->lang = sanitize_languagecode(Yii::app()->request->getPost('loginlang')); - $user->save(); - $sLanguage=$user->lang; - } - else if ($user->lang=='auto' || $user->lang=='') - { - $sLanguage= getBrowserLanguage(); - } - else - { - $sLanguage=$user->lang; - } - - Yii::app()->session['adminlang'] = $sLanguage; - $this->getController()->lang= new limesurvey_lang($sLanguage); - } - - /** - * Checks if the user is using default password - */ - private function _checkForUsageOfDefaultPassword() - { - $clang = $this->getController()->lang; - Yii::app()->session['pw_notify'] = false; - if (strtolower(Yii::app()->request->getPost('password','') ) === 'password') - { - Yii::app()->session['pw_notify'] = true; - Yii::app()->session['flashmessage'] = $clang->gT('Warning: You are still using the default password (\'password\'). Please change your password and re-login again.'); - } - } - - /** - * Get the authentication failed error messages - * @return array Data - */ - private function _getAuthenticationFailedErrorMessage() - { - $clang = $this->getController()->lang; - $aData = array(); - - $userHostAddress = Yii::app()->request->getUserHostAddress(); - $bUserNotFound = Failed_login_attempts::model()->addAttempt($userHostAddress); - - if ($bUserNotFound) - { - $aData['errormsg'] = $clang->gT('Incorrect username and/or password!'); - $aData['maxattempts'] = ''; - } - - $bLockedOut = Failed_login_attempts::model()->isLockedOut($userHostAddress); - - if ($bLockedOut) - { - $aData['maxattempts'] = sprintf( - $clang->gT('You have exceeded the number of maximum login attempts. Please wait %d minutes before trying again.'), - Yii::app()->getConfig('timeOutTime') / 60 - ); - } - - return $aData; + $returnUrl = App()->user->getReturnUrl('/admin'); + $this->getController()->redirect($returnUrl); } /** diff --git a/application/core/LSAuthResult.php b/application/core/LSAuthResult.php index d76b35a1fc5..a96df947aa2 100644 --- a/application/core/LSAuthResult.php +++ b/application/core/LSAuthResult.php @@ -16,7 +16,7 @@ class LSAuthResult protected $_code; protected $_message; - public function __construct($code = 0, $message = 'Ok') { + public function __construct($code = 0, $message = '') { $this->setError($code, $message); } @@ -29,6 +29,16 @@ public function isValid() return false; } + public function getCode() + { + return $this->_code; + } + + public function getMessage() + { + return $this->_message; + } + public function setError($code, $message = null) { $this->_code = $code; $this->_message = $message; diff --git a/application/core/LSUserIdentity.php b/application/core/LSUserIdentity.php index 176d3291061..e94b9cd6120 100644 --- a/application/core/LSUserIdentity.php +++ b/application/core/LSUserIdentity.php @@ -16,19 +16,23 @@ class LSUserIdentity extends CUserIdentity { const ERROR_IP_LOCKED_OUT = 98; const ERROR_UNKNOWN_HANDLER = 99; + protected $config = array(); + /** * The userid * * @var int */ - protected $id = null; + public $id = null; + + public $plugin = null; /** * A User::model() object * * @var User */ - protected $user; + public $user; /** * This is the name of the plugin to handle authentication @@ -40,44 +44,120 @@ class LSUserIdentity extends CUserIdentity { public function authenticate() { // First initialize the result, we can later retieve it to get the exact error code/message - $this->_result = new LSAuthResult(); + $result = new LSAuthResult(self::ERROR_NONE); // Check if the ip is locked out if (Failed_login_attempts::model()->isLockedOut()) { - $this->_result->setError(self::ERROR_IP_LOCKED_OUT); + $message = sprintf(gT('You have exceeded the number of maximum login attempts. Please wait %d minutes before trying again.'), App()->getConfig('timeOutTime') / 60); + $result->setError(self::ERROR_IP_LOCKED_OUT, $message); } // If still ok, continue - if ($this->_result->isValid()) + if ($result->isValid()) { if (is_null($this->plugin)) { - $this->_result->setError(self::ERROR_UNKNOWN_HANDLER); + $result->setError(self::ERROR_UNKNOWN_HANDLER); } else { // Delegate actual authentication to plugin - $authEvent = new PluginEvent('newSession', $this); + $authEvent = new PluginEvent('newUserSession', $this); App()->getPluginManager()->dispatchEvent($authEvent, array($this->plugin)); - $result = $authEvent->get('result'); - if ($result instanceof LSAuthResult) { - + $pluginResult = $authEvent->get('result'); + if ($pluginResult instanceof LSAuthResult) { + $result = $pluginResult; + } else { + $result->setError(self::ERROR_UNKNOWN_IDENTITY); } } } - @session_regenerate_id(); // Prevent session fixation - if ($this->_result->isValid()) { + if ($result->isValid()) { // Perform postlogin $this->postLogin(); - } else { // Log a failed attempt $userHostAddress = App()->request->getUserHostAddress(); Failed_login_attempts::model()->addAttempt($userHostAddress); + App()->session->regenerateID(); // Handled on login by Yii } - return $this->_result->isValid(); + $this->errorCode = $result->getCode(); + $this->errorMessage = $result->getMessage(); + + return $result->isValid(); + } + + public function getConfig() + { + return $this->config; + } + + /** + * Returns the current user's ID + * + * @access public + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * Returns the active user's record + * + * @access public + * @return User + */ + public function getUser() + { + return $this->user; + } + + protected function postLogin() + { + $user = $this->getUser(); + App()->user->login($this); + + // Check for default password + if ($this->password === 'password') { + App()->user->setFlash('pwdnotify', gT('Warning: You are still using the default password (\'password\'). Please change your password and re-login again.')); + } + + // Do session setup + Yii::app()->session['loginID'] = (int) $user->uid; + Yii::app()->session['user'] = $user->users_name; + Yii::app()->session['full_name'] = $user->full_name; + Yii::app()->session['htmleditormode'] = $user->htmleditormode; + Yii::app()->session['templateeditormode'] = $user->templateeditormode; + Yii::app()->session['questionselectormode'] = $user->questionselectormode; + Yii::app()->session['dateformat'] = $user->dateformat; + Yii::app()->session['session_hash'] = hash('sha256',getGlobalSetting('SessionName').$user->users_name.$user->uid); + + // Perform language settings + if (App()->request->getPost('loginlang','default') != 'default') + { + $user->lang = sanitize_languagecode(App()->request->getPost('loginlang')); + $user->save(); + $sLanguage=$user->lang; + } + else if ($user->lang=='auto' || $user->lang=='') + { + $sLanguage=getBrowserLanguage(); + } + else + { + $sLanguage=$user->lang; + } + + Yii::app()->session['adminlang'] = $sLanguage; + App()->getController()->lang= new limesurvey_lang($sLanguage); } public function setPlugin($name) { $this->plugin = $name; } + + public function setConfig($config) { + $this->config = $config; + } } \ No newline at end of file diff --git a/application/core/plugins/Authdb/Authdb.php b/application/core/plugins/Authdb/Authdb.php index 6ef7b889baa..3f3806982f4 100644 --- a/application/core/plugins/Authdb/Authdb.php +++ b/application/core/plugins/Authdb/Authdb.php @@ -12,38 +12,104 @@ public function __construct(PluginManager $manager, $id) { * Here you should handle subscribing to the events your plugin will handle */ $this->subscribe('beforeLogin'); - $this->subscribe('afterCreateLoginForm'); + $this->subscribe('newLoginForm'); + $this->subscribe('afterLoginFormSubmit'); + $this->subscribe('newUserSession'); } public function beforeLogin(PluginEvent $event) { + $event->set('default', get_class($this)); // This is the default login method, should be configurable from plugin settings + // We can skip the login form here and set username/password etc. - /* @var $identity UserIdentity */ + /* @var $identity LSUserIdentity */ $identity = $event->get('identity'); - if (App()->getRequest()->isPostRequest() && !is_null(Yii::app()->request->getQuery('onepass'))) { + if (App()->getRequest()->getIsPostRequest() && !is_null(Yii::app()->request->getQuery('onepass'))) { // We have a one time password, skip the login form - $identity->onepass = Yii::app()->getRequest()->getQuery('onepass'); + $identity->setConfig(array('onepass'=>Yii::app()->getRequest()->getQuery('onepass'))); $identity->username = Yii::app()->getRequest()->getQuery('user'); $event->stop(); // Skip the login form } } - public function afterCreateLoginForm(PluginEvent $event) + public function newLoginForm(PluginEvent $event) { - // Here we can influence the way the login form looks - $blocks = $event->get('blocks', array()); + $event->getContent($this) + ->addContent(CHtml::tag('li', array(), "")) + ->addContent(CHtml::tag('li', array(), "")); + } + + public function afterLoginFormSubmit(PluginEvent $event) + { + // Here we handle moving post data to the identity + /* @var $identity LSUserIdentity */ + $identity = $event->get('identity'); - $blocks['Authdb'] = ''; - $event->set('blocks', $blocks); + $request = App()->getRequest(); + if ($request->getIsPostRequest()) { + $identity->username = $request->getPost('user'); + $identity->password = $request->getPost('password'); + } + + $event->set('identity', $identity); } - public function afterLoginPost(PluginEvent $event) + public function newUserSession(PluginEvent $event) { - // Here we handle the authentication, using the posted form data - $blocks = $event->get('blocks', array()); - $event->set('blocks', $blocks); + // Here we do the actual authentication + /* @var $identity LSUserIdentity */ + $identity = $event->getSender(); + + $username = $identity->username; + $password = $identity->password; + $config = $identity->getConfig(); + $onepass = isset($config['onepass']) ? $config['onepass'] : ''; + + $user = User::model()->findByAttributes(array('users_name' => $username)); + + if ($user !== null) + { + if (gettype($user->password)=='resource') + { + $sStoredPassword=stream_get_contents($user->password,-1,0); // Postgres delivers bytea fields as streams :-o + } + else + { + $sStoredPassword=$user->password; + } + } + else + { + $event->set('result', new LSAuthResult(LSUserIdentity::ERROR_USERNAME_INVALID)); + return; + } + + if ($onepass != '' && Yii::app()->getConfig("use_one_time_passwords") && md5($onepass) == $user->one_time_pw) + { + $user->one_time_pw=''; + $user->save(); + $identity->id = $user->uid; + $identity->user = $user; + $event->set('result', new LSAuthResult(LSUserIdentity::ERROR_NONE)); + return; + } + + + if ($sStoredPassword !== hash('sha256', $password)) + { + $event->set('result', new LSAuthResult(LSUserIdentity::ERROR_PASSWORD_INVALID)); + return; + } + else + { + $identity->id = $user->uid; + $identity->user = $user; + $event->set('result', new LSAuthResult(LSUserIdentity::ERROR_NONE)); + return; + } + } diff --git a/application/views/admin/authentication/login.php b/application/views/admin/authentication/login.php index b4dafda585d..051238e4950 100644 --- a/application/views/admin/authentication/login.php +++ b/application/views/admin/authentication/login.php @@ -3,10 +3,26 @@