From 5cac196a64eb7e2aa1f73cfbaf5e70fd7967740f Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Sun, 21 Jul 2013 02:41:47 +0000
Subject: [PATCH] Add a password reset implementation

Uses a seven step procedure:
  1. (user) Fails to login twice or more
  2. Clicks the 'Forgot my password' link on the login form
  3. Submits the username or email address and triggers a password-reset
     email
  4. Clicks the link in the email and is directed back to the reset page
  5. Enters the username or email again and is logged in
  6. Password change is forced, but current password is not required
  7. Password is updated, user can continue the session without
     authenticating again
---
 include/ajax.content.php              |   6 +-
 include/class.config.php              |  28 +++++++
 include/class.staff.php               | 115 ++++++++++++++++++++------
 include/class.template.php            |   3 +
 include/staff/login.header.php        |  22 +++++
 include/staff/login.tpl.php           |  28 ++-----
 include/staff/profile.inc.php         |   2 +
 include/staff/pwreset.login.php       |  26 ++++++
 include/staff/pwreset.php             |  25 ++++++
 include/staff/pwreset.sent.php        |  22 +++++
 include/staff/settings-system.inc.php |  29 ++++++-
 include/staff/tpl.inc.php             |   8 +-
 scp/login.php                         |   2 +-
 scp/pwreset.php                       |  88 ++++++++++++++++++++
 scp/staff.inc.php                     |  17 +++-
 15 files changed, 360 insertions(+), 61 deletions(-)
 create mode 100644 include/staff/login.header.php
 create mode 100644 include/staff/pwreset.login.php
 create mode 100644 include/staff/pwreset.php
 create mode 100644 include/staff/pwreset.sent.php
 create mode 100644 scp/pwreset.php

diff --git a/include/ajax.content.php b/include/ajax.content.php
index 7ba1a1d4c..197d75ffe 100644
--- a/include/ajax.content.php
+++ b/include/ajax.content.php
@@ -15,9 +15,9 @@
 **********************************************************************/
 
 if(!defined('INCLUDE_DIR')) die('!');
-	    
+
 class ContentAjaxAPI extends AjaxController {
-   
+
     function log($id) {
 
         if($id && ($log=Log::lookup($id))) {
@@ -77,6 +77,8 @@ class ContentAjaxAPI extends AjaxController {
                     <tr><td>%{assignee}</td><td>Assigned staff/team</td></tr>
                     <tr><td>%{assigner}</td><td>Staff assigning the ticket</td></tr>
                     <tr><td>%{url}</td><td>osTicket\'s base url (FQDN)</td></tr>
+                    <tr><td>%{reset_link}</td>
+                        <td>Reset link used by the password reset feature</td></tr>
                 </table>
             </td>
         </tr>
diff --git a/include/class.config.php b/include/class.config.php
index 3aaf9d716..372630987 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -474,6 +474,30 @@ class OsticketConfig extends Config {
         return ($this->get('staff_ip_binding'));
     }
 
+    /**
+     * Configuration: allow_pw_reset
+     *
+     * TRUE if the <a>Forgot my password</a> link and system should be
+     * enabled, and FALSE otherwise.
+     */
+    function allowPasswordReset() {
+        return $this->get('allow_pw_reset');
+    }
+
+    /**
+     * Configuration: pw_reset_window
+     *
+     * Number of minutes for which the password reset token is valid.
+     *
+     * Returns: Number of seconds the password reset token is valid. The
+     *      number of minutes from the database is automatically converted
+     *      to seconds here.
+     */
+    function getPwResetWindow() {
+        // pw_reset_window is stored in minutes. Return value in seconds
+        return $this->get('pw_reset_window') * 60;
+    }
+
     function isCaptchaEnabled() {
         return (extension_loaded('gd') && function_exists('gd_info') && $this->get('enable_captcha'));
     }
@@ -744,6 +768,8 @@ class OsticketConfig extends Config {
         $f['datetime_format']=array('type'=>'string',   'required'=>1, 'error'=>'Datetime format required');
         $f['daydatetime_format']=array('type'=>'string',   'required'=>1, 'error'=>'Day, Datetime format required');
         $f['default_timezone_id']=array('type'=>'int',   'required'=>1, 'error'=>'Default Timezone required');
+        $f['pw_reset_window']=array('type'=>'int', 'required'=>1, 'min'=>1,
+            'error'=>'Valid password reset window required');
 
 
         if(!Validator::process($f, $vars, $errors) || $errors)
@@ -766,6 +792,8 @@ class OsticketConfig extends Config {
             'client_max_logins'=>$vars['client_max_logins'],
             'client_login_timeout'=>$vars['client_login_timeout'],
             'client_session_timeout'=>$vars['client_session_timeout'],
+            'allow_pw_reset'=>isset($vars['allow_pw_reset'])?1:0,
+            'pw_reset_window'=>$vars['pw_reset_window'],
             'time_format'=>$vars['time_format'],
             'date_format'=>$vars['date_format'],
             'datetime_format'=>$vars['datetime_format'],
diff --git a/include/class.staff.php b/include/class.staff.php
index a4edb4f7a..19bd71bf0 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -15,6 +15,7 @@
 **********************************************************************/
 include_once(INCLUDE_DIR.'class.ticket.php');
 include_once(INCLUDE_DIR.'class.dept.php');
+include_once(INCLUDE_DIR.'class.error.php');
 include_once(INCLUDE_DIR.'class.team.php');
 include_once(INCLUDE_DIR.'class.group.php');
 include_once(INCLUDE_DIR.'class.passwd.php');
@@ -401,6 +402,7 @@ class Staff {
 
     //Staff profile update...unfortunately we have to separate it from admin update to avoid potential issues
     function updateProfile($vars, &$errors) {
+        global $cfg;
 
         $vars['firstname']=Format::striptags($vars['firstname']);
         $vars['lastname']=Format::striptags($vars['lastname']);
@@ -437,7 +439,17 @@ class Staff {
             elseif($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2']))
                 $errors['passwd2']='Password(s) do not match';
 
-            if(!$vars['cpasswd'])
+            if (($rtoken = $_SESSION['_staff']['reset-token'])) {
+                $_config = new Config('pwreset');
+                if ($_config->get($rtoken) != $this->getId())
+                    $errors['err'] =
+                        'Invalid reset token. Logout and try again';
+                elseif (!($ts = $_config->lastModified($rtoken))
+                        && ($cfg->getPwResetWindow() < (time() - strtotime($ts))))
+                    $errors['err'] =
+                        'Invalid reset token. Logout and try again';
+            }
+            elseif(!$vars['cpasswd'])
                 $errors['cpasswd']='Current password required';
             elseif(!$this->cmp_passwd($vars['cpasswd']))
                 $errors['cpasswd']='Invalid current password!';
@@ -470,8 +482,10 @@ class Staff {
             .' ,default_paper_size='.db_input($vars['default_paper_size']);
 
 
-        if($vars['passwd1'])
+        if($vars['passwd1']) {
             $sql.=' ,change_passwd=0, passwdreset=NOW(), passwd='.db_input(Passwd::hash($vars['passwd1']));
+            $this->cancelResetTokens();
+        }
 
         $sql.=' WHERE staff_id='.db_input($this->getId());
 
@@ -576,7 +590,7 @@ class Staff {
     }
 
     function lookup($id) {
-        return ($id && is_numeric($id) && ($staff= new Staff($id)) && $staff->getId()==$id)?$staff:null;
+        return ($id && ($staff= new Staff($id)) && $staff->getId()) ? $staff : null;
     }
 
     function login($username, $passwd, &$errors, $strike=true) {
@@ -600,31 +614,10 @@ class Staff {
         if($errors) return false;
 
         if(($user=new StaffSession(trim($username))) && $user->getId() && $user->check_passwd($passwd)) {
-            //update last login && password reset stuff.
-            $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() ';
-            if($user->isPasswdResetDue() && !$user->isAdmin())
-                $sql.=',change_passwd=1';
-            $sql.=' WHERE staff_id='.db_input($user->getId());
-            db_query($sql);
-            //Now set session crap and lets roll baby!
-            $_SESSION['_staff'] = array(); //clear.
-            $_SESSION['_staff']['userID'] = $username;
-            $user->refreshSession(); //set the hash.
-            $_SESSION['TZ_OFFSET'] = $user->getTZoffset();
-            $_SESSION['TZ_DST'] = $user->observeDaylight();
-
-            //Log debug info.
-            $ost->logDebug('Staff login',
-                    sprintf("%s logged in [%s]", $user->getUserName(), $_SERVER['REMOTE_ADDR'])); //Debug.
-
-            //Regenerate session id.
-            $sid=session_id(); //Current id
-            session_regenerate_id(TRUE);
-            //Destroy old session ID - needed for PHP version < 5.1.0 TODO: remove when we move to php 5.3 as min. requirement.
-            if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id())
-                $session->destroy($sid);
+            self::_do_login($user, $username);
 
             Signal::send('auth.login.succeeded', $user);
+            $user->cancelResetTokens();
 
             return $user;
         }
@@ -651,6 +644,36 @@ class Staff {
         return false;
     }
 
+    function _do_login($user, $username) {
+        global $ost;
+
+        //update last login && password reset stuff.
+        $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() ';
+        if($user->isPasswdResetDue() && !$user->isAdmin())
+            $sql.=',change_passwd=1';
+        $sql.=' WHERE staff_id='.db_input($user->getId());
+        db_query($sql);
+        //Now set session crap and lets roll baby!
+        $_SESSION['_staff'] = array(); //clear.
+        $_SESSION['_staff']['userID'] = $username;
+        $user->refreshSession(); //set the hash.
+        $_SESSION['TZ_OFFSET'] = $user->getTZoffset();
+        $_SESSION['TZ_DST'] = $user->observeDaylight();
+
+        //Log debug info.
+        $ost->logDebug('Staff login',
+                sprintf("%s logged in [%s]", $user->getUserName(), $_SERVER['REMOTE_ADDR'])); //Debug.
+
+        //Regenerate session id.
+        $sid=session_id(); //Current id
+        session_regenerate_id(TRUE);
+        //Destroy old session ID - needed for PHP version < 5.1.0 TODO: remove when we move to php 5.3 as min. requirement.
+        if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id())
+            $session->destroy($sid);
+
+        return $user;
+    }
+
     function create($vars, &$errors) {
         if(($id=self::save(0, $vars, $errors)) && $vars['teams'] && ($staff=Staff::lookup($id))) {
             $staff->updateTeams($vars['teams']);
@@ -660,6 +683,43 @@ class Staff {
         return $id;
     }
 
+    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);
+        unset($_SESSION['_staff']['reset-token']);
+    }
+
+    function sendResetEmail() {
+        global $ost, $cfg;
+
+        if(!($tpl = $this->getDept()->getTemplate()))
+            $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');
+
+        $msg = $ost->replaceTemplateVariables($template->asArray(), array(
+            'url' => $ost->getConfig()->getBaseUrl(),
+            'token' => $token,
+            'reset_link' => sprintf(
+                "%s/scp/pwreset.php?token=%s",
+                $ost->getConfig()->getBaseUrl(),
+                $token),
+        ));
+
+        if(!($email=$cfg->getAlertEmail()))
+            $email =$cfg->getDefaultEmail();
+
+        $_config = new Config('pwreset');
+        $_config->set($token, $this->getId());
+
+        $email->send($this->getEmail(), $msg['subj'], $msg['body']);
+    }
+
     function save($id, $vars, &$errors) {
 
         $vars['username']=Format::striptags($vars['username']);
@@ -736,8 +796,9 @@ class Staff {
             .' ,signature='.db_input($vars['signature'])
             .' ,notes='.db_input($vars['notes']);
 
-        if($vars['passwd1'])
+        if($vars['passwd1']) {
             $sql.=' ,passwd='.db_input(Passwd::hash($vars['passwd1']));
+        }
 
         if(isset($vars['change_passwd']))
             $sql.=' ,change_passwd=1';
diff --git a/include/class.template.php b/include/class.template.php
index c85297d2f..c270b41bf 100644
--- a/include/class.template.php
+++ b/include/class.template.php
@@ -56,6 +56,9 @@ class EmailTemplateGroup {
         'ticket.overdue'=>array(
             'name'=>'Overdue Ticket Alert',
             'desc'=>'Alert sent to staff on stale or overdue tickets.'),
+        'staff.pwreset' => array(
+            'name' => 'Staff Password Reset',
+            'desc' => 'Notice sent to staff with the password reset link.'),
         );
 
     function EmailTemplateGroup($id){
diff --git a/include/staff/login.header.php b/include/staff/login.header.php
new file mode 100644
index 000000000..679a509f9
--- /dev/null
+++ b/include/staff/login.header.php
@@ -0,0 +1,22 @@
+<?php
+defined('OSTSCPINC') or die('Invalid path');
+?>
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+    <title>osTicket:: SCP Login</title>
+    <link rel="stylesheet" href="css/login.css" type="text/css" />
+    <meta name="robots" content="noindex" />
+    <meta http-equiv="cache-control" content="no-cache" />
+    <meta http-equiv="pragma" content="no-cache" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
+    <script type="text/javascript" src="../js/jquery-1.7.2.min.js"></script>
+    <script type="text/javascript">
+        $(document).ready(function() {
+            $("input:not(.dp):visible:enabled:first").focus();
+         });
+    </script>
+</head>
+<body id="loginBody">
+
diff --git a/include/staff/login.tpl.php b/include/staff/login.tpl.php
index b8b136eb5..6d5435732 100644
--- a/include/staff/login.tpl.php
+++ b/include/staff/login.tpl.php
@@ -1,26 +1,7 @@
-<?php 
-defined('OSTSCPINC') or die('Invalid path');
-
+<?php
+include_once(INCLUDE_DIR.'staff/login.header.php');
 $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
 ?>
-<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head>
-    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
-    <title>osTicket:: SCP Login</title>
-    <link rel="stylesheet" href="css/login.css" type="text/css" />
-    <meta name="robots" content="noindex" />
-    <meta http-equiv="cache-control" content="no-cache" />
-    <meta http-equiv="pragma" content="no-cache" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
-    <script type="text/javascript" src="../js/jquery-1.7.2.min.js"></script>
-    <script type="text/javascript">
-        $(document).ready(function() {
-            $("input:not(.dp):visible:enabled:first").focus();
-         });
-    </script>
-</head>
-<body id="loginBody">
 <div id="loginBox">
     <h1 id="logo"><a href="index.php">osTicket Staff Control Panel</a></h1>
     <h3><?php echo Format::htmlchars($msg); ?></h3>
@@ -28,9 +9,12 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
         <?php csrf_token(); ?>
         <input type="hidden" name="do" value="scplogin">
         <fieldset>
-            <input type="text" name="username" id="name" value="<?php echo $info['username']; ?>" placeholder="username" autocorrect="off" autocapitalize="off">
+            <input type="text" name="userid" id="name" value="<?php echo $info['username']; ?>" placeholder="username" autocorrect="off" autocapitalize="off">
             <input type="password" name="passwd" id="pass" placeholder="password" autocorrect="off" autocapitalize="off">
         </fieldset>
+        <?php if ($_SESSION['_staff']['strikes'] > 1 && $cfg->allowPasswordReset()) { ?>
+        <h3 style="display:inline"><a href="pwreset.php">Forgot my password</a></h3>
+        <?php } ?>
         <input class="submit" type="submit" name="submit" value="Log In">
     </form>
 </div>
diff --git a/include/staff/profile.inc.php b/include/staff/profile.inc.php
index 073a7c8a4..2543c80d1 100644
--- a/include/staff/profile.inc.php
+++ b/include/staff/profile.inc.php
@@ -190,6 +190,7 @@ $info['id']=$staff->getId();
                 <em><strong>Password</strong>: To reset your password, provide your current password and a new password below.&nbsp;<span class="error">&nbsp;<?php echo $errors['passwd']; ?></span></em>
             </th>
         </tr>
+        <?php if (!isset($_SESSION['_staff']['reset-token'])) { ?>
         <tr>
             <td width="180">
                 Current Password:
@@ -199,6 +200,7 @@ $info['id']=$staff->getId();
                 &nbsp;<span class="error">&nbsp;<?php echo $errors['cpasswd']; ?></span>
             </td>
         </tr>
+        <?php } ?>
         <tr>
             <td width="180">
                 New Password:
diff --git a/include/staff/pwreset.login.php b/include/staff/pwreset.login.php
new file mode 100644
index 000000000..6f93f1f01
--- /dev/null
+++ b/include/staff/pwreset.login.php
@@ -0,0 +1,26 @@
+<?php
+include_once(INCLUDE_DIR.'staff/login.header.php');
+defined('OSTSCPINC') or die('Invalid path');
+$info = ($_POST)?Format::htmlchars($_POST):array();
+?>
+
+<div id="loginBox">
+    <h1 id="logo"><a href="index.php">osTicket Staff Password Reset</a></h1>
+    <h3><?php echo Format::htmlchars($msg); ?></h3>
+
+    <form action="pwreset.php" method="post">
+        <?php csrf_token(); ?>
+        <input type="hidden" name="do" value="newpasswd"/>
+        <input type="hidden" name="token" value="<?php echo $_REQUEST['token']; ?>"/>
+        <fieldset>
+            <input type="text" name="userid" id="name" value="<?php echo
+                $info['userid']; ?>" placeholder="username or email"
+                autocorrect="off" autocapitalize="off"/>
+        </fieldset>
+        <input class="submit" type="submit" name="submit" value="Login"/>
+    </form>
+</div>
+
+<div id="copyRights">Copyright &copy; <a href='http://www.osticket.com' target="_blank">osTicket.com</a></div>
+</body>
+</html>
diff --git a/include/staff/pwreset.php b/include/staff/pwreset.php
new file mode 100644
index 000000000..6aadeb2fc
--- /dev/null
+++ b/include/staff/pwreset.php
@@ -0,0 +1,25 @@
+<?php
+include_once(INCLUDE_DIR.'staff/login.header.php');
+defined('OSTSCPINC') or die('Invalid path');
+$info = ($_POST && $errors)?Format::htmlchars($_POST):array();
+?>
+
+<div id="loginBox">
+    <h1 id="logo"><a href="index.php">osTicket Staff Password Reset</a></h1>
+    <h3><?php echo Format::htmlchars($msg); ?></h3>
+    <form action="pwreset.php" method="post">
+        <?php csrf_token(); ?>
+        <input type="hidden" name="do" value="sendmail">
+        <fieldset>
+            <input type="text" name="userid" id="name" value="<?php echo
+                $info['userid']; ?>" placeholder="username" autocorrect="off"
+                autocapitalize="off">
+        </fieldset>
+        <input class="submit" type="submit" name="submit" value="Send Email"/>
+    </form>
+
+</div>
+
+<div id="copyRights">Copyright &copy; <a href='http://www.osticket.com' target="_blank">osTicket.com</a></div>
+</body>
+</html>
diff --git a/include/staff/pwreset.sent.php b/include/staff/pwreset.sent.php
new file mode 100644
index 000000000..832b78ef5
--- /dev/null
+++ b/include/staff/pwreset.sent.php
@@ -0,0 +1,22 @@
+<?php
+include_once(INCLUDE_DIR.'staff/login.header.php');
+defined('OSTSCPINC') or die('Invalid path');
+$info = ($_POST && $errors)?Format::htmlchars($_POST):array();
+?>
+
+<div id="loginBox">
+    <h1 id="logo"><a href="index.php">osTicket Staff Password Reset</a></h1>
+    <h3>A confirmation email has been sent</h3>
+    <h3 style="color:black;"><em>
+    A password reset email was sent to the email on file for your account.
+    Follow the link in the email to reset your password.
+    </em></h3>
+
+    <form action="index.php" method="get">
+        <input class="submit" type="submit" name="submit" value="Login"/>
+    </form>
+</div>
+
+<div id="copyRights">Copyright &copy; <a href='http://www.osticket.com' target="_blank">osTicket.com</a></div>
+</body>
+</html>
diff --git a/include/staff/settings-system.inc.php b/include/staff/settings-system.inc.php
index 6fade6d96..8915c8b4f 100644
--- a/include/staff/settings-system.inc.php
+++ b/include/staff/settings-system.inc.php
@@ -112,7 +112,12 @@ $gmtime = Misc::gmtime();
                 </select>
             </td>
         </tr>
-        <tr><td>Password Reset Policy:</th>
+        <tr>
+            <th colspan="2">
+                <em><b>Authentication Settings</b></em>
+            </th>
+        </tr>
+        <tr><td>Password Change Policy:</th>
             <td>
                 <select name="passwd_reset_period">
                    <option value="0"> &mdash; None &mdash;</option>
@@ -126,10 +131,20 @@ $gmtime = Misc::gmtime();
                 &nbsp;<font class="error">&nbsp;<?php echo $errors['passwd_reset_period']; ?></font>
             </td>
         </tr>
-        <tr><td>Bind Staff Session to IP:</td>
+        <tr><td>Allow Password Resets:</th>
             <td>
-              <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>>
-              <em>(binds staff session to originating IP address upon login)</em>
+              <input type="checkbox" name="allow_pw_reset" <?php echo $config['allow_pw_reset']?'checked="checked"':''; ?>>
+              <em>Enables the <u>Forgot my password</u> link on the staff
+              control panel</em>
+            </td>
+        </tr>
+        <tr><td>Password Reset Window:</th>
+            <td>
+              <input type="text" name="pw_reset_window" size="6" value="<?php
+                    echo $config['pw_reset_window']; ?>">
+                Maximum time <em>in minutes</em> a password reset token can
+                be valid.
+                &nbsp;<font class="error">&nbsp;<?php echo $errors['pw_reset_window']; ?></font>
             </td>
         </tr>
         <tr><td>Staff Excessive Logins:</td>
@@ -182,6 +197,12 @@ $gmtime = Misc::gmtime();
                 &nbsp;Maximum idle time in minutes before a client must log in again (enter 0 to disable).
             </td>
         </tr>
+        <tr><td>Bind Staff Session to IP:</td>
+            <td>
+              <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>>
+              <em>(binds staff session to originating IP address upon login)</em>
+            </td>
+        </tr>
         <tr>
             <th colspan="2">
                 <em><b>Date and Time Options</b>: Please refer to <a href="http://php.net/date" target="_blank">PHP Manual</a> for supported parameters.</em>
diff --git a/include/staff/tpl.inc.php b/include/staff/tpl.inc.php
index 8c1ede75e..58e0b57fc 100644
--- a/include/staff/tpl.inc.php
+++ b/include/staff/tpl.inc.php
@@ -6,6 +6,7 @@ if (is_a($template, EmailTemplateGroup)) {
     $id = 0;
     $tpl_id = $template->getId();
     $name = $template->getName();
+    $group = $template;
     $selected = $_REQUEST['code_name'];
     $action = 'implement';
     $extras = array('code_name'=>$selected, 'tpl_id'=>$tpl_id);
@@ -15,6 +16,7 @@ if (is_a($template, EmailTemplateGroup)) {
     $id = $template->getId();
     $tpl_id = $template->getTplId();
     $name = $template->getGroup()->getName();
+    $group = $template->getGroup();
     $selected = $template->getCodeName();
     $action = 'updatetpl';
     $extras = array();
@@ -33,7 +35,7 @@ $tpl=$msgtemplates[$info['tpl']];
     <select id="tpl_options" name="id" style="width:300px;">
         <option value="">&mdash; Select Setting Group &mdash;</option>
         <?php
-        foreach($template->getGroup()->getTemplates() as $cn=>$t) {
+        foreach($group->getTemplates() as $cn=>$t) {
             $nfo=$t->getDescription();
             if (!$nfo['name'])
                 continue;
@@ -41,6 +43,10 @@ $tpl=$msgtemplates[$info['tpl']];
             echo sprintf('<option value="%s" %s>%s</option>',
                     $t->getId(),$sel,$nfo['name']);
         }
+        if ($id == 0) { ?>
+            <option selected="selected" value="<?php echo $id; ?>"><?php
+            echo $msgtemplates[$selected]['name']; ?></option>
+        <?php }
         ?>
     </select>
     <input type="submit" value="Go">
diff --git a/scp/login.php b/scp/login.php
index fcefaafd6..2f3cf2236 100644
--- a/scp/login.php
+++ b/scp/login.php
@@ -24,7 +24,7 @@ $msg = $_SESSION['_staff']['auth']['msg'];
 $msg = $msg?$msg:'Authentication Required';
 if($_POST) {
     //$_SESSION['_staff']=array(); #Uncomment to disable login strikes.
-    if(($user=Staff::login($_POST['username'], $_POST['passwd'], $errors))){
+    if(($user=Staff::login($_POST['userid'], $_POST['passwd'], $errors))){
         $dest=($dest && (!strstr($dest,'login.php') && !strstr($dest,'ajax.php')))?$dest:'index.php';
         @header("Location: $dest");
         require_once('index.php'); //Just incase header is messed up.
diff --git a/scp/pwreset.php b/scp/pwreset.php
new file mode 100644
index 000000000..a8efb2f6e
--- /dev/null
+++ b/scp/pwreset.php
@@ -0,0 +1,88 @@
+<?php
+/*********************************************************************
+    pwreset.php
+
+    Handles step 2, 3 and 5 of password resetting
+        1. Fail to login (2+ fail login attempts)
+        2. Visit password reset form and enter username or email
+        3. Receive an email with a link and follow it
+        4. Visit password reset form again, with the link
+        5. Enter the username or email address again and login
+        6. Password change is now required, user changes password and
+           continues on with the session
+
+    Peter Rotich <peter@osticket.com>
+    Jared Hancock <jared@osticket.com>
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+require_once('../main.inc.php');
+if(!defined('INCLUDE_DIR')) die('Fatal Error. Kwaheri!');
+
+require_once(INCLUDE_DIR.'class.staff.php');
+require_once(INCLUDE_DIR.'class.csrf.php');
+
+$tpl = 'pwreset.php';
+if($_POST) {
+    if (!$ost->checkCSRFToken()) {
+        Http::response(400, 'Valid CSRF Token Required');
+        exit;
+    }
+    switch ($_POST['do']) {
+        case 'sendmail':
+            if (($staff=Staff::lookup($_POST['userid']))) {
+                if (!$staff->sendResetEmail()) {
+                    $tpl = 'pwreset.sent.php';
+                }
+            }
+            else
+                $msg = 'Unable to verify username '
+                    .Format::htmlchars($_POST['userid']);
+            break;
+        case 'newpasswd':
+            // TODO: Compare passwords
+            $tpl = 'pwreset.login.php';
+            $_config = new Config('pwreset');
+            if (($staff = new StaffSession($_POST['userid'])) &&
+                    !$staff->getId())
+                $msg = 'Invalid user-id given';
+            elseif (!($id = $_config->get($_POST['token']))
+                    || $id != $staff->getId())
+                $msg = 'Invalid reset token';
+            elseif (!($ts = $_config->lastModified($_POST['token']))
+                    && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
+                $msg = 'Invalid reset token';
+            elseif (!$staff->forcePasswdRest())
+                $msg = 'Unable to reset password';
+            else {
+                Staff::_do_login($staff, $_POST['userid']);
+                $_SESSION['_staff']['reset-token'] = $_POST['token'];
+                header('Location: index.php');
+                exit();
+            }
+            break;
+    }
+}
+elseif ($_GET['token']) {
+    $msg = 'Re-enter your username or email';
+    $_config = new Config('pwreset');
+    if (($id = $_config->get($_GET['token']))
+            && ($staff = Staff::lookup($id)))
+        $tpl = 'pwreset.login.php';
+    else
+        header('Location: index.php');
+}
+elseif ($cfg->allowPasswordReset()) {
+    $msg = 'Enter your username or email address below';
+}
+else {
+    $_SESSION['_staff']['auth']['msg']='Password resets are disabled';
+    return header('Location: index.php');
+}
+define("OSTSCPINC",TRUE); //Make includes happy!
+include_once(INCLUDE_DIR.'staff/'. $tpl);
diff --git a/scp/staff.inc.php b/scp/staff.inc.php
index 577fdd12c..503c3cd41 100644
--- a/scp/staff.inc.php
+++ b/scp/staff.inc.php
@@ -1,7 +1,7 @@
 <?php
 /*************************************************************************
     staff.inc.php
-    
+
     File included on every staff page...handles logins (security) and file path issues.
 
     Peter Rotich <peter@osticket.com>
@@ -42,13 +42,14 @@ require_once(INCLUDE_DIR.'class.nav.php');
 require_once(INCLUDE_DIR.'class.csrf.php');
 
 /* First order of the day is see if the user is logged in and with a valid session.
-    * User must be valid staff beyond this point 
+    * User must be valid staff beyond this point
     * ONLY super admins can access the helpdesk on offline state.
 */
 
 
 if(!function_exists('staffLoginPage')) { //Ajax interface can pre-declare the function to  trap expired sessions.
     function staffLoginPage($msg) {
+        global $ost, $cfg;
         $_SESSION['_staff']['auth']['dest']=THISURI;
         $_SESSION['_staff']['auth']['msg']=$msg;
         require(SCP_DIR.'login.php');
@@ -59,7 +60,15 @@ if(!function_exists('staffLoginPage')) { //Ajax interface can pre-declare the fu
 $thisstaff = new StaffSession($_SESSION['_staff']['userID']); //Set staff object.
 //1) is the user Logged in for real && is staff.
 if(!$thisstaff || !is_object($thisstaff) || !$thisstaff->getId() || !$thisstaff->isValid()){
-    $msg=(!$thisstaff || !$thisstaff->isValid())?'Authentication Required':'Session timed out due to inactivity';
+    if (isset($_SESSION['_staff']['auth']['msg'])) {
+        $msg = $_SESSION['_staff']['auth']['msg'];
+        unset($_SESSION['_staff']['auth']['msg']);
+    }
+    elseif ($thisstaff && !$thisstaff->isValid())
+        $msg = 'Session timed out due to inactivity';
+    else
+        $msg = 'Authentication Required';
+
     staffLoginPage($msg);
     exit;
 }
@@ -88,7 +97,7 @@ if ($_POST  && !$ost->checkCSRFToken()) {
     exit;
 }
 
-//Add token to the header - used on ajax calls [DO NOT CHANGE THE NAME] 
+//Add token to the header - used on ajax calls [DO NOT CHANGE THE NAME]
 $ost->addExtraHeader('<meta name="csrf_token" content="'.$ost->getCSRFToken().'" />');
 
 /******* SET STAFF DEFAULTS **********/
-- 
GitLab