From d899848c45732c9d6904c3fb51f973eeb5866a91 Mon Sep 17 00:00:00 2001 From: ralfhartmann Date: Sun, 13 Oct 2013 19:54:28 +0200 Subject: [PATCH 1/2] add static User::checkCryptPassword() for use in ModuleLogin checks "crypt" passwords only --- system/modules/core/library/Contao/User.php | 23 ++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/system/modules/core/library/Contao/User.php b/system/modules/core/library/Contao/User.php index a8d8a1db5f..a00e2083a5 100644 --- a/system/modules/core/library/Contao/User.php +++ b/system/modules/core/library/Contao/User.php @@ -217,6 +217,27 @@ public function authenticate() } + /** + * + * Checks "crypt" passwords only + * + * @param string $strCrypted The crypted password + * @param string $strPlain The plain password + * + * @return boolean true, if the passwords "match", else false + * + */ + public static function checkCryptPassword($strCrypted, $strPlain) + { + if (\Encryption::test($strCrypted)) + { + return (crypt($strPlain, $strCrypted) == $strCrypted); + } + + return false; + } + + /** * Try to login the current user * @@ -303,7 +324,7 @@ public function login() // The password has been generated with crypt() if (\Encryption::test($this->password)) { - $blnAuthenticated = (crypt(\Input::post('password', true), $this->password) == $this->password); + $blnAuthenticated = \User::checkCryptPassword($this->password, \Input::post('password', true)); } else { From 5e36b533b94d53743c9a9b645fe2602fd2129859 Mon Sep 17 00:00:00 2001 From: ralfhartmann Date: Sun, 13 Oct 2013 19:56:18 +0200 Subject: [PATCH 2/2] the FE User can be forced to change his password, now TODO: * add a table-layout template * add the translations --- system/modules/core/classes/FrontendUser.php | 13 + system/modules/core/config/autoload.php | 1 + system/modules/core/dca/tl_member.php | 23 +- system/modules/core/dca/tl_module.php | 5 +- system/modules/core/languages/en/default.xlf | 3 + .../modules/core/languages/en/tl_member.xlf | 6 + .../modules/core/languages/en/tl_module.xlf | 6 + system/modules/core/modules/ModuleLogin.php | 226 +++++++++++++----- .../templates/modules/mod_pwchange_1cl.html5 | 35 +++ 9 files changed, 258 insertions(+), 60 deletions(-) create mode 100644 system/modules/core/templates/modules/mod_pwchange_1cl.html5 diff --git a/system/modules/core/classes/FrontendUser.php b/system/modules/core/classes/FrontendUser.php index 681c9e2a09..d9e75d3e0f 100644 --- a/system/modules/core/classes/FrontendUser.php +++ b/system/modules/core/classes/FrontendUser.php @@ -157,6 +157,18 @@ public function authenticate() // Default authentication if (parent::authenticate()) { + // Check if the user has to change the password + if ($this->pwChange) + { + // we are here because of the login module, do not logout + if (\Session::getInstance()->get('PASSWORD_CHANGE_REQUIRED')) + { + \Session::getInstance()->remove('PASSWORD_CHANGE_REQUIRED'); + return false; + } + $this->logout(); + $this->reload(); + } return true; } @@ -184,6 +196,7 @@ public function authenticate() return false; } + /* was called by parent */ $this->setUserFromDb(); // Last login date diff --git a/system/modules/core/config/autoload.php b/system/modules/core/config/autoload.php index fb005481bd..dab452ffcc 100644 --- a/system/modules/core/config/autoload.php +++ b/system/modules/core/config/autoload.php @@ -324,6 +324,7 @@ 'mod_html' => 'system/modules/core/templates/modules', 'mod_login_1cl' => 'system/modules/core/templates/modules', 'mod_login_2cl' => 'system/modules/core/templates/modules', + 'mod_pwchange_1cl' => 'system/modules/core/templates/modules', 'mod_logout_1cl' => 'system/modules/core/templates/modules', 'mod_logout_2cl' => 'system/modules/core/templates/modules', 'mod_message' => 'system/modules/core/templates/modules', diff --git a/system/modules/core/dca/tl_member.php b/system/modules/core/dca/tl_member.php index 5bd780eac8..e8c3f57306 100644 --- a/system/modules/core/dca/tl_member.php +++ b/system/modules/core/dca/tl_member.php @@ -117,7 +117,7 @@ // Subpalettes 'subpalettes' => array ( - 'login' => 'username,password', + 'login' => 'pwChange,username,password', 'assignDir' => 'homeDir' ), @@ -305,7 +305,7 @@ 'exclude' => true, 'filter' => true, 'inputType' => 'checkbox', - 'eval' => array('submitOnChange'=>true), + 'eval' => array('submitOnChange'=>true, 'tl_class'=>'w50 clr'), 'sql' => "char(1) NOT NULL default ''" ), 'username' => array @@ -316,7 +316,7 @@ 'sorting' => true, 'flag' => 1, 'inputType' => 'text', - 'eval' => array('mandatory'=>true, 'unique'=>true, 'rgxp'=>'extnd', 'nospace'=>true, 'maxlength'=>64, 'feEditable'=>true, 'feViewable'=>true, 'feGroup'=>'login'), + 'eval' => array('mandatory'=>true, 'unique'=>true, 'rgxp'=>'extnd', 'nospace'=>true, 'maxlength'=>64, 'feEditable'=>true, 'feViewable'=>true, 'feGroup'=>'login', 'tl_class'=>'w50 clr'), 'sql' => "varchar(64) COLLATE utf8_bin NOT NULL default ''" ), 'password' => array @@ -331,6 +331,23 @@ ), 'sql' => "varchar(128) NOT NULL default ''" ), + 'pwChange' => array + ( + 'label' => &$GLOBALS['TL_LANG']['tl_member']['pwChange'], + 'exclude' => true, + 'inputType' => 'checkbox', + 'filter' => true, + 'sql' => "char(1) NOT NULL default ''", + 'eval' => array('tl_class'=>'w50') + ), + 'oldPasswords' => array + ( + 'label' => &$GLOBALS['TL_LANG']['MSC']['oldpasswords'], + 'exclude' => true, + 'inputType' => 'none', + 'eval' => array('preserveTags'=>true, 'beEditable' => false, 'feEditable'=>false, 'feGroup'=>'login'), + 'sql' => "blob NULL" + ), 'assignDir' => array ( 'label' => &$GLOBALS['TL_LANG']['tl_member']['assignDir'], diff --git a/system/modules/core/dca/tl_module.php b/system/modules/core/dca/tl_module.php index 0821e5e6de..41eb1aa1fa 100644 --- a/system/modules/core/dca/tl_module.php +++ b/system/modules/core/dca/tl_module.php @@ -113,7 +113,7 @@ 'login' => '{title_legend},name,headline,type;{config_legend},autologin;{redirect_legend},jumpTo,redirectBack;{template_legend:hide},cols;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID,space', 'logout' => '{title_legend},name,headline,type;{redirect_legend},jumpTo,redirectBack;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID,space', 'personalData' => '{title_legend},name,headline,type;{config_legend},editable;{redirect_legend},jumpTo;{template_legend:hide},memberTpl,tableless;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID,space', - 'registration' => '{title_legend},name,headline,type;{config_legend},editable,newsletters,disableCaptcha;{account_legend},reg_groups,reg_allowLogin,reg_assignDir;{redirect_legend},jumpTo;{email_legend:hide},reg_activate;{template_legend:hide},memberTpl,tableless;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID,space', + 'registration' => '{title_legend},name,headline,type;{config_legend},editable,newsletters,disableCaptcha;{account_legend},reg_groups,reg_allowLogin,reg_pwChange,reg_assignDir;{redirect_legend},jumpTo;{email_legend:hide},reg_activate;{template_legend:hide},memberTpl,tableless;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID,space', 'lostPassword' => '{title_legend},name,headline,type;{config_legend},reg_skipName,disableCaptcha;{redirect_legend},jumpTo;{email_legend:hide},reg_jumpTo,reg_password;{template_legend:hide},tableless;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID,space', 'closeAccount' => '{title_legend},name,headline,type;{config_legend},reg_close;{redirect_legend},jumpTo;{template_legend:hide},tableless;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID,space', 'form' => '{title_legend},name,headline,type;{include_legend},form;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID,space', @@ -632,7 +632,8 @@ 'label' => &$GLOBALS['TL_LANG']['tl_module']['reg_allowLogin'], 'exclude' => true, 'inputType' => 'checkbox', - 'sql' => "char(1) NOT NULL default ''" + 'sql' => "char(1) NOT NULL default ''", + 'eval' => array('tl_class'=>'w50 clr') ), 'reg_skipName' => array ( diff --git a/system/modules/core/languages/en/default.xlf b/system/modules/core/languages/en/default.xlf index 47bc289d09..c663eaa269 100644 --- a/system/modules/core/languages/en/default.xlf +++ b/system/modules/core/languages/en/default.xlf @@ -1424,6 +1424,9 @@ This e-mail has been generated by Contao. You can not reply to it directly. Please enter your username and password! + + Please enter a password! + Confirmation diff --git a/system/modules/core/languages/en/tl_member.xlf b/system/modules/core/languages/en/tl_member.xlf index 3e402ce9fc..c6cc2a05e6 100644 --- a/system/modules/core/languages/en/tl_member.xlf +++ b/system/modules/core/languages/en/tl_member.xlf @@ -215,6 +215,12 @@ Activate/deactivate member ID %s + + Password change required + + + Make the member change his password upon the next login. + diff --git a/system/modules/core/languages/en/tl_module.xlf b/system/modules/core/languages/en/tl_module.xlf index 51814adb6b..f2e93a3dad 100644 --- a/system/modules/core/languages/en/tl_module.xlf +++ b/system/modules/core/languages/en/tl_module.xlf @@ -581,6 +581,12 @@ Please click ##link## to set the new password. If you did not request this e-mai Paste after module ID %s + + The administrator wants you to change your password. + + + This password has already been used. + diff --git a/system/modules/core/modules/ModuleLogin.php b/system/modules/core/modules/ModuleLogin.php index 8c47bfa889..b55abe93b6 100644 --- a/system/modules/core/modules/ModuleLogin.php +++ b/system/modules/core/modules/ModuleLogin.php @@ -35,6 +35,105 @@ class ModuleLogin extends \Module protected $strTemplate = 'mod_login_1cl'; + protected function proceedLogin() + { + $objMember = \MemberModel::findByUsername(\Input::post('username')); + + $strRedirect = \Environment::get('request'); + + // Redirect to the last page visited + if ($this->redirectBack && $_SESSION['LAST_PAGE_VISITED'] != '') + { + $strRedirect = $_SESSION['LAST_PAGE_VISITED']; + } + else + { + // Redirect to the jumpTo page + if ($this->jumpTo && ($objTarget = $this->objModel->getRelated('jumpTo')) !== null) + { + $strRedirect = $this->generateFrontendUrl($objTarget->row()); + } + + // Overwrite the jumpTo page with an individual group setting + if ($objMember !== null) + { + $arrGroups = deserialize($objMember->groups); + + if (!empty($arrGroups) && is_array($arrGroups)) + { + $objGroupPage = \MemberGroupModel::findFirstActiveWithJumpToByIds($arrGroups); + + if ($objGroupPage !== null) + { + $strRedirect = $this->generateFrontendUrl($objGroupPage->row()); + } + } + } + } + + // Auto login is not allowed + if (isset($_POST['autologin']) && !$this->autologin) + { + unset($_POST['autologin']); + \Input::setPost('autologin', null); + } + + // Login and redirect + $this->import('FrontendUser', 'User'); + if ($this->User->login()) + { + if ($this->User->pwChange) + { + \Session::getInstance()->set('PASSWORD_CHANGE_REQUIRED', true); + $this->reload(); + } + $this->redirect($strRedirect); + } + + $this->reload(); + } + + + /** + * Change the password if it is new + * + * @param integer $minAged min seconds to reuse a old password + * + * @return boolean + */ + protected function changePassword($minAged = 10368000 /* 24*60*60*120 = 120 days */) + { + $strPlainPassword = \Input::post($this->formPassword->name); + $this->import('FrontendUser', 'User'); + $theMember = \MemberModel::findByUsername($this->User->username); + + if (\User::checkCryptPassword($theMember->password, $strPlainPassword)) + { + return false; + } + + $arrOldPasswords = deserialize($theMember->oldPasswords, true); + foreach ($arrOldPasswords as $password) + { + if (\User::checkCryptPassword($password['value'], $strPlainPassword)) + { + if ($password['tstamp'] > time()-$minAged) + { + return false; + } + } + } + + $arrOldPasswords[] = array('value' => $theMember->password, 'tstamp' => time()); + $theMember->oldPasswords = serialize($arrOldPasswords); + $theMember->password = $this->formPassword->value; + $theMember->pwChange = ''; + $theMember->save(); + + return true; + } + + /** * Display a login form * @return string @@ -60,6 +159,12 @@ public function generate() $_SESSION['LAST_PAGE_VISITED'] = $this->getReferer(); } + $this->formPassword = new \FormPassword(array( + 'name' => 'password', + 'label' => &$GLOBALS['TL_LANG']['MSC']['pw_change'], + 'mandatory' => true + ) ); + // Login if (\Input::post('FORM_SUBMIT') == 'tl_login') { @@ -70,55 +175,37 @@ public function generate() $this->reload(); } - $this->import('FrontendUser', 'User'); - $strRedirect = \Environment::get('request'); + $this->proceedLogin(); + } - // Redirect to the last page visited - if ($this->redirectBack && $_SESSION['LAST_PAGE_VISITED'] != '') + if (\Input::post('FORM_SUBMIT') == 'tl_pwchange') + { + if (\Input::post('tl_logout')) { - $strRedirect = $_SESSION['LAST_PAGE_VISITED']; + \Input::setPost('FORM_SUBMIT', 'tl_logout'); } else { - // Redirect to the jumpTo page - if ($this->jumpTo && ($objTarget = $this->objModel->getRelated('jumpTo')) !== null) + // Check password + $this->validate = $this->formPassword->validate(); + if ($this->formPassword->hasErrors()) { - $strRedirect = $this->generateFrontendUrl($objTarget->row()); + $_SESSION['LOGIN_ERROR'] = $this->formPassword->getErrorAsString(0); + \Session::getInstance()->set('PASSWORD_CHANGE_REQUIRED', true); + $this->reload(); } - - // Overwrite the jumpTo page with an individual group setting - $objMember = \MemberModel::findByUsername(\Input::post('username')); - - if ($objMember !== null) + if (!$this->changePassword()) { - $arrGroups = deserialize($objMember->groups); - - if (!empty($arrGroups) && is_array($arrGroups)) - { - $objGroupPage = \MemberGroupModel::findFirstActiveWithJumpToByIds($arrGroups); - - if ($objGroupPage !== null) - { - $strRedirect = $this->generateFrontendUrl($objGroupPage->row()); - } - } + $this->loadLanguageFile('tl_module'); + $_SESSION['LOGIN_ERROR'] = specialchars($GLOBALS['TL_LANG']['tl_module']['old_password_forbidden']); + \Session::getInstance()->set('PASSWORD_CHANGE_REQUIRED', true); + $this->reload(); } - } - // Auto login is not allowed - if (isset($_POST['autologin']) && !$this->autologin) - { - unset($_POST['autologin']); - \Input::setPost('autologin', null); + $this->import('FrontendUser', 'User'); + \Input::setPost('username', $this->User->username); + $this->proceedLogin(); } - - // Login and redirect - if ($this->User->login()) - { - $this->redirect($strRedirect); - } - - $this->reload(); } // Logout and redirect to the website root if the current page is protected @@ -142,6 +229,7 @@ public function generate() } // Logout and redirect + $this->import('FrontendUser', 'User'); if ($this->User->logout()) { $this->redirect($strRedirect); @@ -159,29 +247,27 @@ public function generate() */ protected function compile() { - // Show logout form + // set Template and may import User if (FE_USER_LOGGED_IN) { $this->import('FrontendUser', 'User'); - $this->strTemplate = ($this->cols > 1) ? 'mod_logout_2cl' : 'mod_logout_1cl'; - - $this->Template = new \FrontendTemplate($this->strTemplate); - $this->Template->setData($this->arrData); - - $this->Template->slabel = specialchars($GLOBALS['TL_LANG']['MSC']['logout']); - $this->Template->loggedInAs = sprintf($GLOBALS['TL_LANG']['MSC']['loggedInAs'], $this->User->username); - $this->Template->action = ampersand(\Environment::get('indexFreeRequest')); - - if ($this->User->lastLogin > 0) + if ($this->User->pwChange) { - global $objPage; - $this->Template->lastLogin = sprintf($GLOBALS['TL_LANG']['MSC']['lastLogin'][1], \Date::parse($objPage->datimFormat, $this->User->lastLogin)); + // Show password change form + \Session::getInstance()->set('PASSWORD_CHANGE_REQUIRED', true); + $this->strTemplate = ($this->cols > 1) ? 'mod_pwchange_2cl' : 'mod_pwchange_1cl'; + } + else + { + // Show logout form + $this->strTemplate = ($this->cols > 1) ? 'mod_logout_2cl' : 'mod_logout_1cl'; } - - return; } - - $this->strTemplate = ($this->cols > 1) ? 'mod_login_2cl' : 'mod_login_1cl'; + else + { + // Show login form + $this->strTemplate = ($this->cols > 1) ? 'mod_login_2cl' : 'mod_login_1cl'; + } $this->Template = new \FrontendTemplate($this->strTemplate); $this->Template->setData($this->arrData); @@ -207,8 +293,38 @@ protected function compile() $this->Template->password = $GLOBALS['TL_LANG']['MSC']['password'][0]; $this->Template->action = ampersand(\Environment::get('indexFreeRequest')); $this->Template->slabel = specialchars($GLOBALS['TL_LANG']['MSC']['login']); + $this->Template->pwclabel = specialchars($GLOBALS['TL_LANG']['MSC']['setNewPassword']); + $this->Template->llabel = specialchars($GLOBALS['TL_LANG']['MSC']['logout']); $this->Template->value = specialchars(\Input::post('username')); $this->Template->autologin = ($this->autologin && $GLOBALS['TL_CONFIG']['autologin'] > 0); $this->Template->autoLabel = $GLOBALS['TL_LANG']['MSC']['autologin']; + + if (FE_USER_LOGGED_IN) + { + $this->Template->loggedInAs = sprintf($GLOBALS['TL_LANG']['MSC']['loggedInAs'], $this->User->username); + + if ($this->User->pwChange) + { + $this->Template->formPassword = $this->formPassword->parse(); + $this->loadLanguageFile('tl_module'); + $this->Template->info = specialchars($GLOBALS['TL_LANG']['tl_module']['password_change_info']); + $this->Template->hasError = $blnHasError; + $this->Template->password = $GLOBALS['TL_LANG']['MSC']['password'][0]; + $this->Template->password_confirm = "\$GLOBALS['TL_LANG']['MSC']['password_confirm'][0]"; + } + else { + $this->Template->slabel = specialchars($GLOBALS['TL_LANG']['MSC']['logout']); + $this->Template->loggedInAs = sprintf($GLOBALS['TL_LANG']['MSC']['loggedInAs'], $this->User->username); + $this->Template->action = ampersand(\Environment::get('indexFreeRequest')); + } + + if ($this->User->lastLogin > 0) + { + global $objPage; + $this->Template->lastLogin = sprintf($GLOBALS['TL_LANG']['MSC']['lastLogin'][1], \Date::parse($objPage->datimFormat, $this->User->lastLogin)); + } + + return; + } } } diff --git a/system/modules/core/templates/modules/mod_pwchange_1cl.html5 b/system/modules/core/templates/modules/mod_pwchange_1cl.html5 new file mode 100644 index 0000000000..1097d4c207 --- /dev/null +++ b/system/modules/core/templates/modules/mod_pwchange_1cl.html5 @@ -0,0 +1,35 @@ + + + +