diff --git a/lib/db.js b/lib/db.js
index 0dca5d2f9f7e01f37ef48b3a1a505e80f733ed88..a3bcb33e7f203c465e5fe6577a970c00ef3b5eab 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -81,6 +81,7 @@ exports.onReady = function(f) {
   'emailType',
   'emailIsVerified',
   'emailsBelongToSameAccount',
+  'lastPasswordReset',
   'haveVerificationSecret',
   'isStaged',
   'lastStaged',
diff --git a/lib/db/json.js b/lib/db/json.js
index a1d8623e4265f1aab1dbf2ba919c75647b4da9e4..6c9555b650f93328ed947c03cc8ba83997ff086c 100644
--- a/lib/db/json.js
+++ b/lib/db/json.js
@@ -33,6 +33,7 @@ var dbPath = path.join(configuration.get('var_path'), "authdb.json");
  *    {
  *      id: <numerical user id>
  *      password: "somepass",
+ *      lastPasswordReset: 123456, (seconds-since-epoch, integer)
  *      emails: {
  *        "lloyd@hilaiel.com": {
  *          type: 'secondary'
@@ -42,6 +43,8 @@ var dbPath = path.join(configuration.get('var_path'), "authdb.json");
  *  ]
  */
 
+function now() { return Math.floor(new Date().getTime() / 1000); }
+
 function getNextUserID() {
   var max = 1;
   jsel.forEach(".id", db.users, function(id) {
@@ -232,6 +235,7 @@ exports.createUserWithPrimaryEmail = function(email, cb) {
   db.users.push({
     id: uid,
     password: null,
+    lastPasswordReset: now(),
     emails: emailVal
   });
   flush();
@@ -306,7 +310,7 @@ exports.completeConfirmEmail = function(secret, cb) {
           exports.emailToUID(o.email, function(err, uid) {
             if(err) return cb(err, o.email, o.existing_user);
 
-            exports.updatePassword(uid, hash, function(err) {
+            exports.updatePassword(uid, hash, true, function(err) {
               cb(err || null, o.email, o.existing_user);
             });
           });
@@ -335,6 +339,7 @@ exports.completeCreateUser = function(secret, cb) {
         db.users.push({
           id: uid,
           password: hash,
+          lastPasswordReset: now(),
           emails: emailVal
         });
         flush();
@@ -385,7 +390,7 @@ exports.completePasswordReset = function(secret, cb) {
         flush();
 
         // update the password!
-        exports.updatePassword(uid, o.passwd, function(err) {
+        exports.updatePassword(uid, o.passwd, true, function(err) {
           cb(err, o.email, uid);
         });
       });
@@ -421,6 +426,17 @@ exports.checkAuth = function(userID, cb) {
   process.nextTick(function() { cb(null, m) });
 };
 
+exports.lastPasswordReset = function(userID, cb) {
+  sync();
+  var m = undefined;
+  if (userID) {
+    m = jsel.match(":root > object:has(:root > .id:expr(x=" + ESC(userID) + ")) > .lastPasswordReset", db.users);
+    if (m.length === 0) m = undefined;
+    else m = m[0];
+  }
+  process.nextTick(function() { cb(null, m) });
+};
+
 exports.userKnown = function(userID, cb) {
   sync();
   var m = jsel.match(":root > object:has(:root > .id:expr(x=" + ESC(userID) + "))", db.users);
@@ -429,12 +445,16 @@ exports.userKnown = function(userID, cb) {
   process.nextTick(function() { cb(null, m) });
 };
 
-exports.updatePassword = function(userID, hash, cb) {
+exports.updatePassword = function(userID, hash, invalidateSessions, cb) {
   sync();
   var m = jsel.match(":root > object:has(.id:expr(x=" + ESC(userID) + "))", db.users);
   var err = undefined;
   if (m.length === 0) err = "no such email address";
-  else m[0].password = hash;
+  else {
+      m[0].password = hash;
+      if (invalidateSessions)
+        m[0].lastPasswordReset = now();
+  }
   flush();
   process.nextTick(function() { cb(err) });
 };
@@ -498,6 +518,7 @@ exports.addTestUser = function(email, hash, cb) {
     db.users.push({
       id: getNextUserID(),
       password: hash,
+      lastPasswordReset: now(),
       emails: emailVal
     });
     flush();
diff --git a/lib/db/mysql.js b/lib/db/mysql.js
index 4b3cc13a2adaea54e84ce205be8c4a0ef2de4677..4c8f1edf36efbd8d10c23e423ac188cd462ba78e 100644
--- a/lib/db/mysql.js
+++ b/lib/db/mysql.js
@@ -9,14 +9,13 @@
 
 /*
  * The Schema:
- *
- *    +--- user ------+       +--- email -----+
- *    |*int id        | <-\   |*int id        |
- *    | string passwd |    \- |*int user      |
- *    +---------------+       |*string address|
- *                            | enum type     |
- *                            | bool verified |
- *                            +---------------+
+ *                                         +--- email -------+
+ *    +--- user --------------------+      |*int    id       |
+ *    |*int       id                |<-----|*int    user     |
+ *    | string    passwd            |      |*string address  |
+ *    | timestamp lastPasswordReset |      | enum   type     |
+ *    +-----------------------------+      | bool   verified |
+ *                                         +-----------------+
  *
  *
  *    +------ staged ----------+
@@ -65,7 +64,8 @@ if (conf.get('env') === 'test_mysql' && process.env['STALL_MYSQL_WHEN_PRESENT'])
 const schemas = [
   "CREATE TABLE IF NOT EXISTS user (" +
     "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
-    "passwd CHAR(64)" +
+    "passwd CHAR(64)," +
+    "lastPasswordReset TIMESTAMP DEFAULT 0 NOT NULL" +
     ") ENGINE=InnoDB;",
 
   "CREATE TABLE IF NOT EXISTS email (" +
@@ -89,6 +89,8 @@ const schemas = [
     ") ENGINE=InnoDB;",
 ];
 
+function now() { return Math.floor(new Date().getTime() / 1000); }
+
 // log an unexpected database error
 function logUnexpectedError(detail) {
   // first, get line number of callee
@@ -369,8 +371,8 @@ exports.completeCreateUser = function(secret, cb) {
 
     // we're creating a new account, add appropriate entries into user and email tables.
     client.query(
-      "INSERT INTO user(passwd) VALUES(?)",
-      [ o.passwd ],
+      "INSERT INTO user(passwd, lastPasswordReset) VALUES(?,FROM_UNIXTIME(?))",
+      [ o.passwd, now() ],
       function(err, info) {
         if (err) return cb(err);
         addEmailToUser(info.insertId, o.email, 'secondary', cb);
@@ -395,7 +397,7 @@ exports.completeConfirmEmail = function(secret, cb) {
     // we're adding or reverifying an email address to an existing user account.  add appropriate
     // entries into email table.
     if (o.passwd) {
-      exports.updatePassword(o.existing_user, o.passwd, function(err) {
+      exports.updatePassword(o.existing_user, o.passwd, true, function(err) {
         if (err) return cb('could not set user\'s password');
         addEmailToUser(o.existing_user, o.email, 'secondary', cb);
       });
@@ -432,7 +434,7 @@ exports.completePasswordReset = function(secret, cb) {
           if (err) return cb(err);
       
           // update the password!
-          exports.updatePassword(uid, o.passwd, function(err) {
+          exports.updatePassword(uid, o.passwd, true, function(err) {
             cb(err, o.email, uid);
           });
         });
@@ -449,7 +451,8 @@ exports.addPrimaryEmailToAccount = function(uid, emailToAdd, cb) {
 exports.createUserWithPrimaryEmail = function(email, cb) {
   // create a new user acct with no password
   client.query(
-    "INSERT INTO user() VALUES()",
+    "INSERT INTO user(lastPasswordReset) VALUES(FROM_UNIXTIME(?))",
+    [ now() ],
     function(err, info) {
       if (err) return cb(err);
       var uid = info.insertId;
@@ -510,10 +513,21 @@ exports.checkAuth = function(uid, cb) {
     });
 }
 
-exports.updatePassword = function(uid, hash, cb) {
+exports.lastPasswordReset = function(uid, cb) {
   client.query(
-    'UPDATE user SET passwd = ? WHERE id = ?',
-    [ hash, uid ],
+    'SELECT UNIX_TIMESTAMP(lastPasswordReset) AS lastPasswordReset FROM user WHERE id = ?',
+    [ uid ],
+    function (err, rows) {
+      cb(err, (rows && rows.length == 1) ? rows[0].lastPasswordReset : undefined);
+    });
+}
+
+exports.updatePassword = function(uid, hash, invalidateSessions, cb) {
+  var query = invalidateSessions ?
+    'UPDATE user SET passwd = ?, lastPasswordReset = FROM_UNIXTIME(?) WHERE id = ?' :
+    'UPDATE user SET passwd = ? WHERE id = ?';
+  var args = invalidateSessions ? [ hash, now(), uid ] : [ hash, uid ];
+  client.query(query, args,
     function (err, rows) {
       if (!err && (!rows || rows.affectedRows !== 1)) {
         err = "no record with id " + uid;
@@ -577,8 +591,8 @@ exports.cancelAccount = function(uid, cb) {
 
 exports.addTestUser = function(email, hash, cb) {
   client.query(
-    "INSERT INTO user(passwd) VALUES(?)",
-    [ hash ],
+    "INSERT INTO user(passwd, lastPasswordReset) VALUES(FROM_UNIXTIME(?))",
+    [ hash, now() ],
     function(err, info) {
       if (err) return cb(err);
 
diff --git a/lib/wsapi.js b/lib/wsapi.js
index 7cee435e2afba35e628923bb3a1c2583447eca40..9d8f8d825e46071fab9fdc0f366c838d282eccc0 100644
--- a/lib/wsapi.js
+++ b/lib/wsapi.js
@@ -29,7 +29,8 @@ path = require('path'),
 validate = require('./validate'),
 statsd = require('./statsd'),
 bcrypt = require('./bcrypt'),
-i18n = require('./i18n');
+i18n = require('./i18n'),
+db = require('./db');
 
 var abide = i18n.abide({
   supported_languages: config.get('supported_languages'),
@@ -79,22 +80,82 @@ function bcryptPassword(password, cb) {
   });
 }
 
-function authenticateSession(session, uid, level, duration_ms) {
+function authenticateSession(options, cb) {
+  var session = options.session;
+  var uid = options.uid;
+  var level = options.level;
+  var duration_ms = options.duration_ms;
   if (['assertion', 'password'].indexOf(level) === -1)
-    throw "invalid authentication level: " + level;
-
-  // if the user is *already* authenticated as this uid with an equal or better
-  // level of auth, let's not lower them.  Issue #1049
-  if (session.userid === uid && session.auth_level === 'password' &&
-      session.auth_level !== level) {
-    logger.info("not resetting cookies to 'assertion' authenticate a user who is already password authenticated");
-  } else {
-    if (duration_ms) {
-      session.setDuration(duration_ms);
+    cb(new Error("invalid authentication level: " + level));
+
+  db.lastPasswordReset(uid, function(err, lastPasswordReset) {
+    if (err)
+      return cb(err);
+    if (lastPasswordReset === undefined)
+      return cb(new Error("authenticateSession called with undefined lastPasswordReset"));
+    // if the user is *already* authenticated as this uid with an equal or
+    // better level of auth, let's not lower them.  Issue #1049
+    if (session.userid === uid && session.auth_level === 'password' &&
+        session.auth_level !== level) {
+      logger.info("not resetting cookies to 'assertion' authenticate a user who is already password authenticated");
+    } else {
+      if (duration_ms) {
+        session.setDuration(duration_ms);
+      }
+      session.userid = uid;
+      session.auth_level = level;
+      session.lastPasswordReset = lastPasswordReset;
     }
-    session.userid = uid;
-    session.auth_level = level;
+    cb(null);
+  });
+}
+
+function checkCSRF(req, resp, next) {
+  // only on POSTs
+  if (req.method !== "POST")
+    return next();
+
+  // there must be a session
+  if (req.session === undefined || typeof req.session.csrf !== 'string') {
+    logger.warn("POST calls to /wsapi require a cookie to be sent, this user may have cookies disabled");
+    return httputils.forbidden(resp, "no cookie");
+  }
+
+  // and the token must match what is sent in the post body
+  if (!req.body || !req.session || !req.session.csrf || req.body.csrf != req.session.csrf) {
+    // if any of these things are false, then we'll block the request
+    var b = req.body ? req.body.csrf : "<none>";
+    var s = req.session ? req.session.csrf : "<none>";
+    logger.warn("CSRF validation failure, token mismatch. got:" + b + " want:" + s);
+    return httputils.badRequest(resp, "CSRF violation");
   }
+
+  // all good
+  next();
+}
+
+function checkExpiredSession(req, resp, next) {
+  // all requests (both GET and POST) must have a session
+  if (req.session === undefined) {
+    logger.warn("calls to /wsapi require a cookie to be sent, this user may have cookies disabled");
+    return httputils.forbidden(resp, "no cookie");
+  }
+  if (!req.session.userid) {
+    // not yet authenticated, so nothing to expire, avoid the DB fetch
+    return next();
+  }
+  db.lastPasswordReset(req.session.userid, function(err, token) {
+    if (err) return databaseDown(resp, err);
+    // if token is 0 (or undefined), they haven't changed their password
+    // since the server was updated to use lastPasswordResets. Allow the
+    // session to pass, otherwise the server upgrade would gratuitously
+    // expire innocent sessions.
+    if (token && token != req.session.lastPasswordReset) {
+      logger.warn("expired cookie (password changed since issued)");
+      req.session.reset();
+    }
+    next();
+  });
 }
 
 function langContext(req) {
@@ -177,60 +238,44 @@ exports.setup = function(options, app) {
     // by layers higher up based on cache control headers.
     // the fallout is that all code that interacts with sessions
     // should be under /wsapi
-    if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX) {
-      // explicitly disallow caching on all /wsapi calls (issue #294)
-      resp.setHeader('Cache-Control', 'no-cache, max-age=0');
+    if (purl.pathname.substr(0, WSAPI_PREFIX.length) !== WSAPI_PREFIX)
+      return next();
 
-      // we set this parameter so the connect-cookie-session
-      // sends the cookie even though the local connection is HTTP
-      // (the load balancer does SSL)
-      if (overSSL)
-        req.connection.proxySecure = true;
+    // explicitly disallow caching on all /wsapi calls (issue #294)
+    resp.setHeader('Cache-Control', 'no-cache, max-age=0');
 
-      const operation = purl.pathname.substr(WSAPI_PREFIX.length);
+    // we set this parameter so the connect-cookie-session
+    // sends the cookie even though the local connection is HTTP
+    // (the load balancer does SSL)
+    if (overSSL)
+      req.connection.proxySecure = true;
 
-      // count the number of WSAPI operation
-      statsd.increment("wsapi." + operation);
-
-      // check to see if the api is known here, before spending more time with
-      // the request.
-      if (!wsapis.hasOwnProperty(operation) ||
-          wsapis[operation].method.toLowerCase() !== req.method.toLowerCase())
-      {
-        // if the fake verification api is enabled (for load testing),
-        // then let this request fall through
-        if (operation !== 'fake_verification' || !process.env['BROWSERID_FAKE_VERIFICATION'])
-          return httputils.badRequest(resp, "no such api");
-      }
+    const operation = purl.pathname.substr(WSAPI_PREFIX.length);
+
+    // count the number of WSAPI operation
+    statsd.increment("wsapi." + operation);
+
+    // check to see if the api is known here, before spending more time with
+    // the request.
+    if (!wsapis.hasOwnProperty(operation) ||
+        wsapis[operation].method.toLowerCase() !== req.method.toLowerCase())
+    {
+      // if the fake verification api is enabled (for load testing),
+      // then let this request fall through
+      if (operation !== 'fake_verification' || !process.env['BROWSERID_FAKE_VERIFICATION'])
+        return httputils.badRequest(resp, "no such api");
+    }
 
-      // perform full parsing and validation
-      return cookieParser(req, resp, function() {
-        bodyParser(req, resp, function() {
-          cookieSessionMiddleware(req, resp, function() {
-            // only on POSTs
-            if (req.method === "POST") {
-
-              if (req.session === undefined || typeof req.session.csrf !== 'string') { // there must be a session
-                logger.warn("POST calls to /wsapi require a cookie to be sent, this user may have cookies disabled");
-                return httputils.forbidden(resp, "no cookie");
-              }
-
-              // and the token must match what is sent in the post body
-              else if (!req.body || !req.session || !req.session.csrf || req.body.csrf != req.session.csrf) {
-                // if any of these things are false, then we'll block the request
-                var b = req.body ? req.body.csrf : "<none>";
-                var s = req.session ? req.session.csrf : "<none>";
-                logger.warn("CSRF validation failure, token mismatch. got:" + b + " want:" + s);
-                return httputils.badRequest(resp, "CSRF violation");
-              }
-            }
-            return next();
+    // perform full parsing and validation
+    return cookieParser(req, resp, function() {
+      bodyParser(req, resp, function() {
+        cookieSessionMiddleware(req, resp, function() {
+          checkExpiredSession(req, resp, function() {
+            return checkCSRF(req, resp, next);
           });
         });
       });
-    } else {
-      return next();
-    }
+    });
   });
 
   // load all of the APIs supported by this process
diff --git a/lib/wsapi/auth_with_assertion.js b/lib/wsapi/auth_with_assertion.js
index 3d76fa43a00e40f437838afa14af4403c3d81965..5a3b1d667264bf9880a6268b6b8c3acea8f3c616 100644
--- a/lib/wsapi/auth_with_assertion.js
+++ b/lib/wsapi/auth_with_assertion.js
@@ -45,10 +45,15 @@ exports.process = function(req, res) {
         return db.emailToUID(email, function(err, uid) {
           if (err) return wsapi.databaseDown(res, err);
           if (!uid) return res.json({ success: false, reason: "internal error" });
-          wsapi.authenticateSession(req.session, uid, 'assertion',
-                                    req.params.ephemeral ? config.get('ephemeral_session_duration_ms')
-                                                       : config.get('authentication_duration_ms'));
-          return res.json({ success: true, userid: uid });
+          wsapi.authenticateSession({session: req.session, uid: uid,
+                                     level: 'assertion',
+                                     duration_ms: req.params.ephemeral ?
+                                     config.get('ephemeral_session_duration_ms')
+                                     : config.get('authentication_duration_ms')
+                                     }, function(err) {
+            if (err) return wsapi.databaseDown(res, err);
+            return res.json({ success: true, userid: uid });
+          });
         });
       }
       else if (type === 'secondary') {
@@ -96,10 +101,15 @@ exports.process = function(req, res) {
           }
 
           logger.info("successfully created primary acct for " + email + " (" + r.userid + ")");
-          wsapi.authenticateSession(req.session, r.userid, 'assertion',
-                                    req.params.ephemeral ? config.get('ephemeral_session_duration_ms')
-                                                       : config.get('authentication_duration_ms'));
-          res.json({ success: true, userid: r.userid });
+          wsapi.authenticateSession({session: req.session, uid: r.userid,
+                                     level: 'assertion',
+                                     duration_ms: req.params.ephemeral ?
+                                     config.get('ephemeral_session_duration_ms')
+                                     : config.get('authentication_duration_ms')
+                                     }, function (err) {
+            if (err) return wsapi.databaseDown(res, err);
+            res.json({ success: true, userid: r.userid });
+          });
         });
       }).on('error', function(e) {
         logger.error("failed to create primary user with assertion for " + email + ": " + e);
diff --git a/lib/wsapi/authenticate_user.js b/lib/wsapi/authenticate_user.js
index 97ee9f325a5035cbf21be6eda320e6c478697109..54ee33e9b19bc10f78c4e2c0f1224c458922b81a 100644
--- a/lib/wsapi/authenticate_user.js
+++ b/lib/wsapi/authenticate_user.js
@@ -64,59 +64,68 @@ exports.process = function(req, res) {
         } else {
           if (!req.session) req.session = {};
 
-          wsapi.authenticateSession(req.session, uid, 'password',
-                                    req.params.ephemeral ? config.get('ephemeral_session_duration_ms')
-                                                         : config.get('authentication_duration_ms'));
-          res.json({ success: true, userid: uid });
-
-
-          // if the work factor has changed, update the hash here.  issue #204
-          // NOTE: this runs asynchronously and will not delay the response
-          if (config.get('bcrypt_work_factor') != bcrypt.getRounds(hash)) {
-            logger.info("updating bcrypted password for user " + uid);
-
-            // this request must be forwarded to dbwriter, and we'll use the
-            // authentication cookie of the user just sent out.
-            var u = wsapi.forwardWritesTo;
-
-            var m = u.scheme === 'http' ? http : https;
-
-            var post_body = querystring.stringify({
-              oldpass: req.params.pass,
-              newpass: req.params.pass,
-              csrf: req.params.csrf
-            });
-            var preq = m.request({
-              host: u.host,
-              port: u.port,
-              path: '/wsapi/update_password',
-              method: "POST",
-              headers: {
-                'Cookie': res._headers['set-cookie'],
-                'Content-Type': 'application/x-www-form-urlencoded',
-                'Content-Length': post_body.length
-              }
-            }, function(pres) {
-              pres.on('end', function() {
-                if (pres.statusCode !== 200) {
-                  logger.error("failed to update bcrypt rounds of password for " + uid +
-                               " dbwriter returns " + pres.statusCode);
-                } else {
-                  logger.info("bcrypt rounds of password for " + uid +
-                              " successfully updated (from " +
-                              bcrypt.getRounds(hash) + " to "
-                              + config.get('bcrypt_work_factor') + ")");
-                }
-              });
-            }).on('error', function(e) {
-              logger.error("failed to update bcrypt rounds of password for " + uid + ": " + e);
-            });
-
-            preq.write(post_body);
-            preq.end();
-          }
+          wsapi.authenticateSession({session: req.session, uid: uid,
+                                     level: 'password',
+                                     duration_ms: req.params.ephemeral ?
+                                      config.get('ephemeral_session_duration_ms')
+                                      : config.get('authentication_duration_ms')
+                                    }, function(err) {
+                                      if (err)
+                                        return wsapi.databaseDown(res, err);
+                                      res.json({ success: true, userid: uid });
+
+                                      // if the work factor has changed, update the hash here.  issue #204
+                                      // NOTE: this runs asynchronously and will not delay the response
+                                      if (config.get('bcrypt_work_factor') != bcrypt.getRounds(hash))
+                                        updateHash(req, res, uid, hash);
+                                    });
         }
       });
     });
   });
 };
+
+
+function updateHash(req, res, uid, hash) {
+  logger.info("updating bcrypted password for user " + uid);
+
+  // this request must be forwarded to dbwriter, and we'll use the
+  // authentication cookie of the user just sent out.
+  var u = wsapi.forwardWritesTo;
+
+  var m = u.scheme === 'http' ? http : https;
+
+  var post_body = querystring.stringify({
+    oldpass: req.params.pass,
+    newpass: req.params.pass,
+    csrf: req.params.csrf
+  });
+  var preq = m.request({
+    host: u.host,
+    port: u.port,
+    path: '/wsapi/update_password',
+    method: "POST",
+    headers: {
+      'Cookie': res._headers['set-cookie'],
+      'Content-Type': 'application/x-www-form-urlencoded',
+      'Content-Length': post_body.length
+    }
+  }, function(pres) {
+    pres.on('end', function() {
+      if (pres.statusCode !== 200) {
+        logger.error("failed to update bcrypt rounds of password for " + uid +
+                     " dbwriter returns " + pres.statusCode);
+      } else {
+        logger.info("bcrypt rounds of password for " + uid +
+                    " successfully updated (from " +
+                    bcrypt.getRounds(hash) + " to "
+                    + config.get('bcrypt_work_factor') + ")");
+      }
+    });
+  }).on('error', function(e) {
+    logger.error("failed to update bcrypt rounds of password for " + uid + ": " + e);
+  });
+
+  preq.write(post_body);
+  preq.end();
+}
diff --git a/lib/wsapi/complete_email_confirmation.js b/lib/wsapi/complete_email_confirmation.js
index f3ba86e791dadd98de37b3ef7e2ccba68ff205ed..816afc7330b758ecf6ff2dfa658fc1a0fe672e8a 100644
--- a/lib/wsapi/complete_email_confirmation.js
+++ b/lib/wsapi/complete_email_confirmation.js
@@ -64,8 +64,13 @@ exports.process = function(req, res) {
           logger.warn("couldn't complete email verification: " + e);
           wsapi.databaseDown(res, e);
         } else {
-          wsapi.authenticateSession(req.session, uid, 'password');
-          res.json({ success: true });
+          wsapi.authenticateSession({session: req.session, uid: uid,
+                                     level: 'password', duration_ms: undefined},
+                                    function(err) {
+                                      if (err)
+                                        return wsapi.databaseDown(res, err);
+                                      res.json({ success: true });
+                                    });
         }
       });
     };
diff --git a/lib/wsapi/complete_reset.js b/lib/wsapi/complete_reset.js
index 49d8b2c58a342ffeb1339aeb3b51738f2489d1c1..6c2e915fd932540b069e3ec574df11d5e883cd3f 100644
--- a/lib/wsapi/complete_reset.js
+++ b/lib/wsapi/complete_reset.js
@@ -82,10 +82,14 @@ exports.process = function(req, res) {
           // At this point, the user is either on the same browser with a token from
           // their email address, OR they've provided their account password.  It's
           // safe to grant them an authenticated session.
-          wsapi.authenticateSession(req.session, uid, 'password',
-                                    config.get('ephemeral_session_duration_ms'));
-
-          res.json({ success: true });
+          wsapi.authenticateSession({session: req.session,
+                                     uid: uid,
+                                     level: 'password',
+                                     duration_ms: config.get('ephemeral_session_duration_ms')
+                                    }, function(err) {
+            if (err) return wsapi.databaseDown(res, err);
+            res.json({ success: true });
+          });
         }
       });
     });
diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js
index 66955d37849514d1ed8a83351507b8458bc5cc8a..2db73cfdb64a0db6c6f3a6694c4360a0d1db2bd6 100644
--- a/lib/wsapi/complete_user_creation.js
+++ b/lib/wsapi/complete_user_creation.js
@@ -87,9 +87,14 @@ exports.process = function(req, res) {
           // At this point, the user is either on the same browser with a token from
           // their email address, OR they've provided their account password.  It's
           // safe to grant them an authenticated session.
-          wsapi.authenticateSession(req.session, uid, 'password',
-                                    config.get('ephemeral_session_duration_ms'));
-          res.json({ success: true });
+          wsapi.authenticateSession({session: req.session,
+                                     uid: uid,
+                                     level: 'password',
+                                     duration_ms: config.get('ephemeral_session_duration_ms')
+                                    }, function(err) {
+            if (err) return wsapi.databaseDown(res, err);
+            res.json({ success: true });
+          });
         }
       });
     });
diff --git a/lib/wsapi/prolong_session.js b/lib/wsapi/prolong_session.js
index 2c9d5c02edb4ab2b9d04ee76ad519573f3c7debb..0fdaecddbf1f8b76483f54fd741a50fa96e1f006 100644
--- a/lib/wsapi/prolong_session.js
+++ b/lib/wsapi/prolong_session.js
@@ -12,7 +12,12 @@ exports.authed = 'assertion';
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  wsapi.authenticateSession(req.session, req.session.userid, req.session.auth_level,
-                            config.get('authentication_duration_ms'));
-  res.send(200);
+  wsapi.authenticateSession({session: req.session,
+                             uid: req.session.userid,
+                             level: req.session.auth_level,
+                             duration_ms: config.get('authentication_duration_ms')
+                             }, function(err) {
+                               if (err) return wsapi.databaseDown(res, err);
+                               res.send(200);
+                             });
 };
diff --git a/lib/wsapi/update_password.js b/lib/wsapi/update_password.js
index 52471aa05c45d18c5baa67ef2dde104855213395..d68a00560c7b1140c74ad4505db67e8adda8e13d 100644
--- a/lib/wsapi/update_password.js
+++ b/lib/wsapi/update_password.js
@@ -55,13 +55,24 @@ exports.process = function(req, res) {
           return res.json({ success: false });
         }
 
-        db.updatePassword(req.session.userid, hash, function(err) {
+        var passwordChanged = (req.params.oldpass != req.params.newpass);
+        db.updatePassword(req.session.userid, hash, passwordChanged,
+                          function(err) {
           var success = true;
           if (err) {
             logger.error("error updating bcrypted password for user " + req.session.userid, err);
             wsapi.databaseDown(res, err);
           } else {
-            res.json({ success: success });
+            // need to update the session
+            wsapi.authenticateSession({session: req.session,
+                                       uid: req.session.userid,
+                                       level: req.session.auth_level,
+                                       duration_ms: req.session.duration_ms
+                                      }, function(err) {
+                                        if (err)
+                                          return wsapi.databaseDown(res, err);
+                                        res.json({ success: success });
+                                      });
           }
         });
       });
diff --git a/lib/wsapi_client.js b/lib/wsapi_client.js
index 81dbce7e09b56896a9dbdccc027eba02d74dbceb..a71202306d74fade5e5053939159e17b78c0db2b 100644
--- a/lib/wsapi_client.js
+++ b/lib/wsapi_client.js
@@ -95,7 +95,8 @@ function withCSRF(cfg, context, cb) {
     exports.get(cfg, '/wsapi/session_context', context, undefined, function(err, r) {
       if (err) return cb(err);
       try {
-        if (r.code !== 200) throw 'http error';
+        if (r.code !== 200)
+            return cb({what: "http error", resp: r}); // report first error
         context.session = JSON.parse(r.body);
         context.sessionStartedAt = new Date().getTime();
         cb(null, context.session.csrf_token);
@@ -109,7 +110,14 @@ function withCSRF(cfg, context, cb) {
 
 exports.post = function(cfg, path, context, postArgs, cb) {
   withCSRF(cfg, context, function(err, csrf) {
-    if (err) return cb(err);
+    if (err) {
+        if (err.what == "http error") {
+            // let the session_context HTTP return code speak for the overall
+            // POST
+            return cb(null, err.resp);
+        }
+        return cb(err);
+    }
 
     // parse the server URL (cfg.browserid)
     var uObj;
diff --git a/tests/lib/wsapi.js b/tests/lib/wsapi.js
index cd35cb64be9a49c0865901ad410e1e9b434c9a32..a64f362d4ed7d6a628ec3c25a764865dfde0b1fc 100644
--- a/tests/lib/wsapi.js
+++ b/tests/lib/wsapi.js
@@ -13,31 +13,32 @@ var configuration = {
   browserid: 'http://127.0.0.1:10002/'
 }
 
-exports.clearCookies = function() {
-  wcli.clearCookies(context);
+exports.clearCookies = function(ctx) {
+  wcli.clearCookies(ctx||context);
 };
 
-exports.injectCookies = function(cookies) {
-  wcli.injectCookies({cookieJar: cookies}, context);
+exports.injectCookies = function(cookies, ctx) {
+  wcli.injectCookies({cookieJar: cookies}, ctx||context);
 };
 
-exports.getCookie = function(which) {
-  return wcli.getCookie(context, which);
+exports.getCookie = function(which, ctx) {
+  return wcli.getCookie(ctx||context, which);
 };
 
-exports.get = function (path, getArgs) {
+exports.get = function (path, getArgs, ctx) {
   return function () {
-    wcli.get(configuration, path, context, getArgs, this.callback);
+    wcli.get(configuration, path, ctx||context, getArgs, this.callback);
   };
 };
 
-exports.post = function (path, postArgs) {
+exports.post = function (path, postArgs, ctx) {
   return function () {
-    wcli.post(configuration, path, context, postArgs, this.callback);
+    wcli.post(configuration, path, ctx||context, postArgs, this.callback);
   };
 };
 
-exports.getCSRF = function() {
+exports.getCSRF = function(ctx) {
+  var context = ctx||context;
   if (context && context.session && context.session.csrf_token) {
     return context.session.csrf_token;
   }
diff --git a/tests/password-update-test.js b/tests/password-update-test.js
index a33ac55680e34f4f5bfc6c2c3c06691b4d77d0a2..e2d9b77a3475a9f5361a862f193a7647cd146208 100755
--- a/tests/password-update-test.js
+++ b/tests/password-update-test.js
@@ -92,6 +92,30 @@ suite.addBatch({
   }
 });
 
+var context2 = {};
+suite.addBatch({
+  "establishing a second session": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: TEST_EMAIL,
+      pass: OLD_PASSWORD,
+      ephemeral: false
+    }, context2),
+    "works as expected": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).success, true);
+    }
+  }
+});
+
+suite.addBatch({
+  "using the second session": {
+    topic: wsapi.post('/wsapi/prolong_session', {}, context2),
+    "works as expected": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(r.body, "OK");
+    }
+  }
+});
+
 suite.addBatch({
   "updating the password without specifying a proper old password": {
     topic: wsapi.post('/wsapi/update_password', {
@@ -117,13 +141,31 @@ suite.addBatch({
 });
 
 suite.addBatch({
-  "updating the password": {
-    topic: wsapi.post('/wsapi/update_password', {
-      oldpass: OLD_PASSWORD,
-      newpass: NEW_PASSWORD
-    }),
-    "works as expected": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).success, true);
+  "after waiting for lastPasswordReset's now() to increment": {
+    topic: function() {
+      // we introduce a 2s delay here to ensure that the now() call in
+      // lib/db/{json,mysql}.js will return a different value than it did
+      // during complete_user_creation(), thus expiring the old session still
+      // hanging out in context2. now() returns an integer
+      // seconds-since-epoch, so the shortest delay that will reliably get a
+      // different result is 1.0s+epsilon (depending upon the resolution of
+      // the system clock). To avoid this stall (and make the test suite run
+      // 2s faster), either:
+      //  1: change now() to include a mutable offset, expose a
+      //     db.addNowOffset() to "accelerate the universe", have this code
+      //     add 1s instead of using setTimeout. Or:
+      //  2: add a db function to modify (increment) lastPasswordReset by 1s,
+      //     have this code call it instead of using setTimeout
+      setTimeout(this.callback, 2000);
+      },
+    "updating the password": {
+      topic: wsapi.post('/wsapi/update_password', {
+        oldpass: OLD_PASSWORD,
+        newpass: NEW_PASSWORD
+      }),
+      "works as expected": function(err, r) {
+        assert.strictEqual(JSON.parse(r.body).success, true);
+      }
     }
   }
 });
@@ -148,6 +190,12 @@ suite.addBatch({
     "fails as expected": function(err, r) {
       assert.strictEqual(JSON.parse(r.body).success, false);
     }
+  },
+  "using the other (expired) session": {
+    topic: wsapi.post('/wsapi/prolong_session', {}, context2),
+    "fails as expected": function(err, r) {
+      assert.strictEqual(r.code, 403);
+    }
   }
 });
 
diff --git a/tests/stalled-mysql-test.js b/tests/stalled-mysql-test.js
index 46b529baa6d2ab661e975122016de5a493a8d005..653ab279592b5e77e58e18e4de12a1617c7466a1 100755
--- a/tests/stalled-mysql-test.js
+++ b/tests/stalled-mysql-test.js
@@ -80,7 +80,6 @@ suite.addBatch({
   "ping": {
     topic: wsapi.get('/wsapi/ping', {}),
     "fails with 500 when db is stalled": function(err, r) {
-      // address info with a primary address doesn't need db access.
       assert.strictEqual(r.code, 500);
     }
   },
@@ -216,7 +215,7 @@ suite.addBatch({
   "ping": {
     topic: wsapi.get('/wsapi/ping', { }),
     "fails": function(err, r) {
-      assert.strictEqual(r.code, 500);
+      assert.strictEqual(r.code, 503);
     }
   },
 
@@ -391,15 +390,11 @@ suite.addBatch({
     "fails with 404": function(err, r) {
       assert.strictEqual(r.code, 404);
     }
-  }
-});
-
-// logout doesn't need database, it should still succeed
-suite.addBatch({
-  "logout": {
+  },
+  "logout": { // logout needs the database too
     topic: wsapi.post('/wsapi/logout', { }),
-    "succeeds": function(err, r) {
-      assert.strictEqual(r.code, 200);
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
     }
   }
 });