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']) {