diff --git a/include/class.ostsession.php b/include/class.ostsession.php index 2b9827b68a330f309e79b493320044a2a88536f7..609bfda0005587f59012603ce03d972cb89de971 100644 --- a/include/class.ostsession.php +++ b/include/class.ostsession.php @@ -182,10 +182,15 @@ extends SessionBackend { function read($id) { try { - $this->data = SessionData::objects()->filter([ - 'session_id' => $id, - 'session_expire__gt' => SqlFunction::NOW(), - ])->one(); + $this->data = SessionData::objects() + ->filter(['session_id' => $id]) + ->annotate(['age' => SqlFunction::NOW()->minus(new SqlField('session_expire'))]) + ->one(); + if ($this->data->age > 0) { + // session_expire is in the past. Pretend it is expired and + // reset the data. This will assist with CSRF issues + $this->data->session_data=''; + } $this->id = $id; } catch (DoesNotExist $e) { diff --git a/include/staff/login.tpl.php b/include/staff/login.tpl.php index 228ec612b0208a0fda92c45cb2439e50a37d7d6f..ca9e751af253d4dab84a91a52722f113fcf63708 100644 --- a/include/staff/login.tpl.php +++ b/include/staff/login.tpl.php @@ -11,9 +11,9 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array(); <span class="valign-helper"></span> <img src="logo.php?login" alt="osTicket :: <?php echo __('Staff Control Panel');?>" /> </a></h1> - <h3><?php echo Format::htmlchars($msg); ?></h3> + <h3 id="login-message"><?php echo Format::htmlchars($msg); ?></h3> <div class="banner"><small><?php echo ($content) ? Format::display($content->getLocalBody()) : ''; ?></small></div> - <form action="login.php" method="post" id="login"> + <form action="login.php" method="post" id="login" onsubmit="attemptLoginAjax(event)"> <?php csrf_token(); ?> <input type="hidden" name="do" value="scplogin"> <fieldset> @@ -21,10 +21,11 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array(); echo $info['userid']; ?>" placeholder="<?php echo __('Email or Username'); ?>" autofocus autocorrect="off" autocapitalize="off"> <input type="password" name="passwd" id="pass" placeholder="<?php echo __('Password'); ?>" autocorrect="off" autocapitalize="off"> - <?php if ($show_reset && $cfg->allowPasswordReset()) { ?> - <h3 style="display:inline"><a href="pwreset.php"><?php echo __('Forgot My Password'); ?></a></h3> - <?php } ?> - <button class="submit button pull-right" type="submit" name="submit"><i class="icon-signin"></i> + <h3 style="display:inline"><a id="reset-link" class="<?php + if (!$show_reset || !$cfg->allowPasswordReset()) echo 'hidden'; + ?>" href="pwreset.php"><?php echo __('Forgot My Password'); ?></a></h3> + <button class="submit button pull-right" type="submit" + name="submit"><i class="icon-signin"></i> <?php echo __('Log In'); ?> </button> </fieldset> @@ -61,11 +62,85 @@ if (count($ext_bks)) { ?> document.getElementById('loginBox').style.backgroundColor = 'white'; } }); + + function attemptLoginAjax(e) { + var objectifyForm = function(formArray) { //serialize data function + var returnArray = {}; + for (var i = 0; i < formArray.length; i++) { + returnArray[formArray[i]['name']] = formArray[i]['value']; + } + return returnArray; + }; + if ($.fn.effect) { + // For some reason, JQuery-UI shake does not considere an element's + // padding when shaking. Looks like it might be fixed in 1.12. + // Thanks, https://stackoverflow.com/a/22302374 + var oldEffect = $.fn.effect; + $.fn.effect = function (effectName) { + if (effectName === "shake") { + var old = $.effects.createWrapper; + $.effects.createWrapper = function (element) { + var result; + var oldCSS = $.fn.css; + + $.fn.css = function (size) { + var _element = this; + var hasOwn = Object.prototype.hasOwnProperty; + return _element === element && hasOwn.call(size, "width") && hasOwn.call(size, "height") && _element || oldCSS.apply(this, arguments); + }; + + result = old.apply(this, arguments); + + $.fn.css = oldCSS; + return result; + }; + } + return oldEffect.apply(this, arguments); + }; + } + var form = $(e.target), + data = objectifyForm(form.serializeArray()) + data.ajax = 1; + $.ajax({ + url: form.attr('action'), + method: 'POST', + data: data, + cache: false, + success: function(json) { + if (!typeof(json) === 'object' || !json.status) + return; + switch (json.status) { + case 401: + if (json && json.redirect) + document.location.href = json.redirect; + if (json && json.message) + $('#login-message').text(json.message) + if (json && json.show_reset) + $('#reset-link').show() + if ($.fn.effect) { + $('#loginBox').effect('shake') + } + // Clear the password field + $('#pass').val('').focus(); + break + case 302: + if (json && json.redirect) + document.location.href = json.redirect; + break + } + }, + }); + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + return false; + } </script> <!--[if IE]> <style> #loginBox:after { background-color: white !important; } </style> <![endif]--> + <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.10.3.custom.min.js"></script> </body> </html> diff --git a/scp/css/login.css b/scp/css/login.css index 71d6cb7fcedac7dbbb217e0d1d977a14efd711ce..3bf8e12c5532ebdace51e11b17357dfbf50b0abd 100644 --- a/scp/css/login.css +++ b/scp/css/login.css @@ -353,3 +353,6 @@ input[type=password] { padding: 5px; font-size: 0.75em; } +.hidden { + display: none; +} diff --git a/scp/login.php b/scp/login.php index 0fc0d0991410c3a2a7ee019aab7ee1c91dd9c4ad..6e1854456c5757f4fdab2b3289728b3cc24a34fc 100644 --- a/scp/login.php +++ b/scp/login.php @@ -30,29 +30,57 @@ $msg = $_SESSION['_staff']['auth']['msg']; $msg = $msg ?: ($content ? $content->getLocalName() : __('Authentication Required')); $dest=($dest && (!strstr($dest,'login.php') && !strstr($dest,'ajax.php')))?$dest:'index.php'; $show_reset = false; -if($_POST) { +if ($_POST) { + $json = isset($_POST['ajax']) && $_POST['ajax']; + $respond = function($code, $message) use ($json, $ost) { + if ($json) { + $payload = is_array($message) ? $message + : array('message' => $message); + $payload['status'] = (int) $code; + Http::response(200, JSONDataEncoder::encode($payload), + 'application/json'); + } + else { + // Extract the `message` portion only + if (is_array($message)) + $message = $message['message']; + Http::response($code, $message); + } + }; + $redirect = function($url) use ($json) { + if ($json) + Http::response(200, JsonDataEncoder::encode(array( + 'status' => 302, 'redirect' => $url)), 'application/json'); + else + Http::redirect($url); + }; + // Check the CSRF token, and ensure that future requests will have to // use a different CSRF token. This will help ward off both parallel and // serial brute force attacks, because new tokens will have to be // requested for each attempt. - if (!$ost->checkCSRFToken()) - Http::response(400, __('Valid CSRF Token Required')); - - // Rotate the CSRF token (original cannot be reused) - $ost->getCSRF()->rotate(); + if (!$ost->checkCSRFToken()) { + $_SESSION['_staff']['auth']['msg'] = __('Valid CSRF Token Required'); + $redirect($_SERVER['REQUEST_URI']); + } // Lookup support backends for this staff $username = trim($_POST['userid']); if ($user = StaffAuthenticationBackend::process($username, $_POST['passwd'], $errors)) { - session_write_close(); - Http::redirect($dest); - require_once('index.php'); //Just incase header is messed up. - exit; + $redirect($dest); } - $msg = $errors['err']?$errors['err']:__('Invalid login'); + $msg = $errors['err'] ?: __('Invalid login'); $show_reset = true; + + if ($json) { + $respond(401, ['message' => $msg, 'show_reset' => $show_reset]); + } + else { + // Rotate the CSRF token (original cannot be reused) + $ost->getCSRF()->rotate(); + } } elseif ($_GET['do']) { switch ($_GET['do']) {