diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css index bc9647ede18c9b98973c77c64c22679a82fc97a0..195460e49c2f317d35be5151f7a54f86a4abb58b 100644 --- a/assets/default/css/theme.css +++ b/assets/default/css/theme.css @@ -626,11 +626,13 @@ label.required { text-align: left; } #clientLogin { - width: 400px; + display: block; margin-top: 20px; - padding: 10px 100px 10px 10px; + padding: 20px; border: 1px solid #ccc; - background: url('../images/lock.png?1319655200') 440px 50% no-repeat #f6f6f6; + border-radius: 5px; + box-shadow: inset 0 1px 2px rgba(0,0,0,0.3); + background: url('../images/lock.png?1319655200') 95% 50% no-repeat #f6f6f6; } #clientLogin p { clear: both; diff --git a/client.inc.php b/client.inc.php index ee9a9fcae443df2d4c0ade4636270d102b9955e8..ad4c7ee4e18c12841a46ac239fd0a7b90bd3e597 100644 --- a/client.inc.php +++ b/client.inc.php @@ -56,7 +56,7 @@ if($thisclient && $thisclient->getId() && $thisclient->isValid()){ /******* CSRF Protectin *************/ // Enforce CSRF protection for POSTS if ($_POST && !$ost->checkCSRFToken()) { - @header('Location: index.php'); + Http::redirect('index.php'); //just incase redirect fails die('Action denied (400)!'); } @@ -68,4 +68,13 @@ $ost->addExtraHeader('<meta name="csrf_token" content="'.$ost->getCSRFToken().'" define('PAGE_LIMIT', DEFAULT_PAGE_LIMIT); $nav = new UserNav($thisclient, 'home'); + +$exempt = in_array(basename($_SERVER['SCRIPT_NAME']), array('logout.php', 'ajax.php', 'logs.php', 'upgrade.php')); + +if (!$exempt && $thisclient && ($acct = $thisclient->getAccount()) + && $acct->isPasswdResetForced()) { + $warn = 'Password change required to continue'; + require('profile.php'); //profile.php must request this file as require_once to avoid problems. + exit; +} ?> diff --git a/include/class.auth.php b/include/class.auth.php index c7b80da05b9ce669359702cbfccc14bd97ffc1d5..74a4664b9165686b7705bc9bf4de73978fb93fe3 100644 --- a/include/class.auth.php +++ b/include/class.auth.php @@ -461,7 +461,6 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend { list($id, $auth) = explode(':', $_SESSION['_auth']['user']['key']); if (!($bk=static::getBackend($id)) //get the backend - || !$bk->supportsAuthentication() //Make sure it can authenticate || !($user=$bk->validate($auth)) //Get AuthicatedUser || !($user instanceof AuthenticatedUser) // Make sure it user || $user->getId() != $_SESSION['_auth']['user']['id'] // check ID @@ -800,12 +799,7 @@ class osTicketClientAuthentication extends UserAuthenticationBackend { static $id = "client"; function authenticate($username, $password) { - if (strpos($username, '@') !== false) - $user = User::lookup(array('emails__address'=>$username)); - else - $user = User::lookup(array('account__username'=>$username)); - - if (!$user) + if (!($user = self::_identify($authkey))) return; if (($client = new ClientSession(new EndUser($user))) @@ -818,18 +812,68 @@ class osTicketClientAuthentication extends UserAuthenticationBackend { } protected function validate($authkey) { + if (!($user = self::_identify($authkey))) + return; + + if (($client = new ClientSession(new EndUser($user))) && $client->getId()) + return $client; + } + + protected function _identify($username) { if (strpos($authkey, '@') !== false) $user = User::lookup(array('emails__address'=>$authkey)); else - $user = User::lookup(array('account__authkey'=>$authkey)); + $user = User::lookup(array('account__username'=>$authkey)); - if (!$user) - return; + return $user; + } - if (($client = new ClientSession(new EndUser($user))) && $client->getId()) +} +UserAuthenticationBackend::register('osTicketClientAuthentication'); + +class ClientPasswordResetTokenBackend extends UserAuthenticationBackend { + static $id = "pwreset.client"; + + function supportsAuthentication() { + return false; + } + + function signOn($errors=array()) { + global $ost; + + if (!isset($_POST['userid']) || !isset($_POST['token'])) + return false; + elseif (!($_config = new Config('pwreset'))) + return false; + elseif (!($acct = ClientAccount::lookupByUsername($_POST['userid'])) + || !$acct->getId() + || !($client = new ClientSession(new EndUser($acct->getUser())))) + $errors['msg'] = 'Invalid user-id given'; + elseif (!($id = $_config->get($_POST['token'])) + || $id != $client->getId()) + $errors['msg'] = 'Invalid reset token'; + elseif (!($ts = $_config->lastModified($_POST['token'])) + && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts)))) + $errors['msg'] = 'Invalid reset token'; + elseif (!$acct->forcePasswdReset()) + $errors['msg'] = 'Unable to reset password'; + else return $client; } + function login($client, $bk) { + $_SESSION['_client']['reset-token'] = $_POST['token']; + Signal::send('auth.pwreset.login', $client); + return parent::login($client, $bk); + } + + protected function validate($authkey) { + if (!($acct = ClientAccount::lookupByUsername($authkey))) + return; + + if (($client = new ClientSession(new EndUser($acct->getUser()))) && $client->getId()) + return $client; + } } -UserAuthenticationBackend::register('osTicketClientAuthentication'); +UserAuthenticationBackend::register('ClientPasswordResetTokenBackend'); ?> diff --git a/include/class.client.php b/include/class.client.php index 309cb78d2f4777b177f414f336e40a0c3c6e5c45..8e5deee12c024e840ebbe87764ece8d4be477770 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -259,7 +259,7 @@ require_once INCLUDE_DIR.'class.orm.php'; class ClientAccountModel extends VerySimpleModel { static $meta = array( 'table' => USER_ACCOUNT_TABLE, - 'pk' => 'id', + 'pk' => array('id'), 'joins' => array( 'user' => array( 'null' => false, @@ -273,8 +273,9 @@ class ClientAccount extends ClientAccountModel { var $_options = null; var $timezone; - const LOCKED = 0x0001; - const PASSWD_RESET_REQUIRED = 0x0002; + const CONFIRMED = 0x0001; + const LOCKED = 0x0002; + const PASSWD_RESET_REQUIRED = 0x0004; function __onload() { if ($this->get('timezone_id')) { @@ -284,6 +285,14 @@ class ClientAccount extends ClientAccountModel { } } + function getId() { + return $this->get('id'); + } + + function getUserId() { + return $this->get('user_id'); + } + function checkPassword($password, $autoupdate=true) { /*bcrypt based password match*/ @@ -308,17 +317,73 @@ class ClientAccount extends ClientAccountModel { return $this->checkPassword($password, false); } + function hasPassword() { + return (bool) $this->get('passwd'); + } + + function sendResetEmail() { + global $ost, $cfg; + + $tpl= $ost->getConfig()->getDefaultTemplate(); + + $token = Misc::randCode(48); // 290-bits + if (!($template = $tpl->getMsgTemplate('staff.pwreset'))) + return new Error('Unable to retrieve password reset email template'); + + $vars = array( + 'url' => $ost->getConfig()->getBaseUrl(), + 'token' => $token, + 'client' => $this, + 'reset_link' => sprintf( + "%s/pwreset.php?token=%s", + $ost->getConfig()->getBaseUrl(), + $token), + ); + + if(!($email=$cfg->getAlertEmail())) + $email = $cfg->getDefaultEmail(); + + $info = array('email' => $email, 'vars' => &$vars, 'log'=>true); + Signal::send('auth.pwreset.email', $this, $info); + + $msg = $ost->replaceTemplateVariables($template->asArray(), $vars); + + $_config = new Config('pwreset'); + $_config->set($vars['token'], $this->user->getId()); + + $email->send($this->user->default_email->get('address'), + $msg['subj'], $msg['body']); + } + function forcePasswdReset() { - $this->set('status', $this->get('status') | self::PASSWD_RESET_REQUIRED); - $this->save(); + $this->_setStatus(self::PASSWD_RESET_REQUIRED); + return $this->save(); + } + + function _getStatus($flag) { + return 0 !== ($this->get('status') & $flag); + } + + function _clearStatus($flag) { + return $this->set('status', $this->get('status') & ~$flag); + } + + function _setStatus($flag) { + return $this->set('status', $this->get('status') | $flag); + } + + function isPasswdResetForced() { + return $this->_getStatus(self::PASSWD_RESET_REQUIRED); } function cancelResetTokens() { // TODO: Drop password-reset tokens from the config table for // this user id $sql = 'DELETE FROM '.CONFIG_TABLE.' WHERE `namespace`="pwreset" - AND `value`='.db_input($this->getId()); - db_query($sql, false); + AND `key`='.db_input($this->getUserId()); + if (!db_query($sql, false)) + return false; + unset($_SESSION['_client']['reset-token']); } @@ -328,19 +393,26 @@ class ClientAccount extends ClientAccountModel { return $base; } + function getUser() { + $user = User::lookup($this->get('user_id')); + $user->set('account', $this); + return $user; + } + function update($vars, &$errors) { - if($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd']) { + $rtoken = $_SESSION['_client']['reset-token']; + if ($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd'] || $rtoken) { - if(!$vars['passwd1']) + if (!$vars['passwd1']) $errors['passwd1']='New password required'; - elseif($vars['passwd1'] && strlen($vars['passwd1'])<6) + elseif ($vars['passwd1'] && strlen($vars['passwd1'])<6) $errors['passwd1']='Must be at least 6 characters'; - elseif($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2'])) + elseif ($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2'])) $errors['passwd2']='Password(s) do not match'; - if (($rtoken = $_SESSION['_client']['reset-token'])) { + if ($rtoken) { $_config = new Config('pwreset'); - if ($_config->get($rtoken) != $this->getId()) + if ($_config->get($rtoken) != $this->getUserId()) $errors['err'] = 'Invalid reset token. Logout and try again'; elseif (!($ts = $_config->lastModified($rtoken)) @@ -348,31 +420,42 @@ class ClientAccount extends ClientAccountModel { $errors['err'] = 'Invalid reset token. Logout and try again'; } - elseif(!$vars['cpasswd']) + elseif (!$vars['cpasswd']) $errors['cpasswd']='Current password required'; - elseif(!$this->hasCurrentPassword($vars['cpasswd'])) + elseif (!$this->hasCurrentPassword($vars['cpasswd'])) $errors['cpasswd']='Invalid current password!'; - elseif(!strcasecmp($vars['passwd1'], $vars['cpasswd'])) + elseif (!strcasecmp($vars['passwd1'], $vars['cpasswd'])) $errors['passwd1']='New password MUST be different from the current password!'; } - if(!$vars['timezone_id']) + if (!$vars['timezone_id']) $errors['timezone_id']='Time zone required'; - if($errors) return false; + if ($errors) return false; $this->set('timezone_id', $vars['timezone_id']); - $this->set('dst',isset($vars['dst'])?1:0); + $this->set('dst', isset($vars['dst']) ? 1 : 0); if ($vars['passwd1']) { $this->set('passwd', Passwd::hash($vars['passwd1'])); $info = array('password' => $vars['passwd1']); Signal::send('auth.pwchange', $this, $info); $this->cancelResetTokens(); + $this->_clearStatus(self::PASSWD_RESET_REQUIRED); } return $this->save(); } + + static function lookupByUsername($username) { + if (strpos($username, '@') !== false) + $user = self::lookup(array('user__emails__address'=>$username)); + else + $user = self::lookup(array('username'=>$username)); + + return $user; + } } +ClientAccount::_inspect(); ?> diff --git a/include/class.user.php b/include/class.user.php index c5b0f5b9e47b67bbf8027cd8af15bc583ea4c7da..c465f095fc3acbf684f888d4578159e80f95272b 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -38,7 +38,7 @@ class UserModel extends VerySimpleModel { ), 'account' => array( 'list' => false, - 'reverse' => 'ClientAccountModel.user', + 'reverse' => 'ClientAccount.user', ), 'default_email' => array( 'null' => true, @@ -147,6 +147,10 @@ class User extends UserModel { return $this->created; } + function getAccount() { + return $this->account; + } + function to_json() { $info = array( diff --git a/include/client/pwreset.login.php b/include/client/pwreset.login.php new file mode 100644 index 0000000000000000000000000000000000000000..a1c1ed4e75f656b8372d95052aa1201ba2e28545 --- /dev/null +++ b/include/client/pwreset.login.php @@ -0,0 +1,26 @@ +<?php +if(!defined('OSTCLIENTINC')) die('Access Denied'); + +$userid=Format::input($_POST['userid']); +?> +<h1>Forgot My Password</h1> +<p> +Enter your username or email address again in the form below and press the +<strong>Login</strong> to access your account and reset your password. + +<form action="pwreset.php" method="post" id="clientLogin"> + <div style="width:50%;display:inline-block"> + <?php csrf_token(); ?> + <input type="hidden" name="do" value="reset"/> + <input type="hidden" name="token" value="<?php echo $_REQUEST['token']; ?>"/> + <strong><?php echo Format::htmlchars($banner); ?></strong> + <br> + <div> + <label for="username">Username:</label> + <input id="username" type="text" name="userid" size="30" value="<?php echo $userid; ?>"> + </div> + <p> + <input class="btn" type="submit" value="Login"> + </p> + </div> +</form> diff --git a/include/client/pwreset.request.php b/include/client/pwreset.request.php new file mode 100644 index 0000000000000000000000000000000000000000..28922c8d2a9d6028ab18ec533ea7be9873c847ec --- /dev/null +++ b/include/client/pwreset.request.php @@ -0,0 +1,26 @@ +<?php +if(!defined('OSTCLIENTINC')) die('Access Denied'); + +$userid=Format::input($_POST['userid']); +?> +<h1>Forgot My Password</h1> +<p> +Enter your username or email address in the form below and press the +<strong>Send Email</strong> button to have a password reset link sent to +your email account on file. + +<form action="pwreset.php" method="post" id="clientLogin"> + <div style="width:50%;display:inline-block"> + <?php csrf_token(); ?> + <input type="hidden" name="do" value="sendmail"/> + <strong><?php echo Format::htmlchars($banner); ?></strong> + <br> + <div> + <label for="username">Username:</label> + <input id="username" type="text" name="userid" size="30" value="<?php echo $userid; ?>"> + </div> + <p> + <input class="btn" type="submit" value="Send Email"> + </p> + </div> +</form> diff --git a/include/client/pwreset.sent.php b/include/client/pwreset.sent.php new file mode 100644 index 0000000000000000000000000000000000000000..c76f50957ecc56ec8a2946a26d729e364d332968 --- /dev/null +++ b/include/client/pwreset.sent.php @@ -0,0 +1,13 @@ +<h1>Forgot My Password</h1> +<p> +Enter your username or email address in the form below and press the +<strong>Send Email</strong> button to have a password reset link sent to +your email account on file. + +<form action="pwreset.php" method="post" id="clientLogin"> + <div style="width:50%;display:inline-block"> + We have sent you a reset email to the email address you have on file for + your account. If you do not receive the email or cannot reset your + password, please ... + </div> +</form> diff --git a/pwreset.php b/pwreset.php new file mode 100644 index 0000000000000000000000000000000000000000..615cbbee95b12566da0311abda30f4a92cdfc61a --- /dev/null +++ b/pwreset.php @@ -0,0 +1,68 @@ +<?php + +require_once('client.inc.php'); +if(!defined('INCLUDE_DIR')) die('Fatal Error'); +define('CLIENTINC_DIR',INCLUDE_DIR.'client/'); +define('OSTCLIENTINC',TRUE); //make includes happy + +require_once(INCLUDE_DIR.'class.client.php'); + +$inc = 'pwreset.request.php'; +if($_POST) { + if (!$ost->checkCSRFToken()) { + Http::response(400, 'Valid CSRF Token Required'); + exit; + } + switch ($_POST['do']) { + case 'sendmail': + if (($acct=ClientAccount::lookupByUsername($_POST['userid']))) { + if (!$acct->hasPassword()) { + $banner = 'Unable to reset password. Contact your administrator'; + } + elseif (!$acct->sendResetEmail()) { + $inc = 'pwreset.sent.php'; + } + } + else + $banner = 'Unable to verify username ' + .Format::htmlchars($_POST['userid']); + break; + case 'create_account': + break; + case 'reset': + $inc = 'pwreset.login.php'; + $errors = array(); + if ($client = UserAuthenticationBackend::processSignOn($errors)) { + Http::redirect('index.php'); + } + elseif (isset($errors['msg'])) { + $banner = $errors['msg']; + } + break; + } +} +elseif ($_GET['token']) { + $banner = 'Re-enter your username or email'; + $_config = new Config('pwreset'); + if (($id = $_config->get($_GET['token'])) + && ($acct = ClientAccount::lookup(array('user_id'=>$id)))) + $inc = 'pwreset.login.php'; + elseif ($id && ($user = User::lookup($id))) + $inc = 'pwreset.create.php'; + else + Http::redirect('index.php'); +} +elseif ($cfg->allowPasswordReset()) { + $banner = 'Enter your username or email address below'; +} +else { + $_SESSION['_staff']['auth']['msg']='Password resets are disabled'; + return header('Location: index.php'); +} + +$nav = new UserNav(); +$nav->setActiveNav('status'); +require CLIENTINC_DIR.'header.inc.php'; +require CLIENTINC_DIR.$inc; +require CLIENTINC_DIR.'footer.inc.php'; +?>