diff --git a/lib/db/json.js b/lib/db/json.js
index e49615c1fb4f5c2f495c9246cb8bcb4dbb695684..376a2a791cae9a968949a202772df5c2193d552f 100644
--- a/lib/db/json.js
+++ b/lib/db/json.js
@@ -236,7 +236,7 @@ exports.emailForVerificationSecret = function(secret, cb) {
   process.nextTick(function() {
     sync();
     if (!db.staged[secret]) return cb("no such secret");
-    cb(null, db.staged[secret].email, db.staged[secret].existing_user);
+    cb(null, db.staged[secret].email, db.staged[secret].existing_user, db.staged[secret].passwd);
   });
 };
 
diff --git a/lib/db/mysql.js b/lib/db/mysql.js
index b2e123ae6667e7c150963f627f0af30ba9c491bc..8c2d805e63fed01488e4b6f82b561198c98369c1 100644
--- a/lib/db/mysql.js
+++ b/lib/db/mysql.js
@@ -265,14 +265,14 @@ exports.haveVerificationSecret = function(secret, cb) {
 
 exports.emailForVerificationSecret = function(secret, cb) {
   client.query(
-    "SELECT email, existing_user FROM staged WHERE secret = ?", [ secret ],
+    "SELECT email, existing_user, passwd FROM staged WHERE secret = ?", [ secret ],
     function(err, rows) {
       if (err) return cb("database unavailable");
 
       // if the record was not found, fail out
       if (!rows || rows.length != 1) return cb("no such secret");
 
-      cb(null, rows[0].email, rows[0].existing_user);
+      cb(null, rows[0].email, rows[0].existing_user, rows[0].passwd);
     });
 };
 
@@ -291,7 +291,7 @@ exports.authForVerificationSecret = function(secret, cb) {
       if (o.passwd) return cb(null, o.passwd, o.existing_user);
 
       // otherwise, let's get the passwd from the user record
-      if (!o.existing_user) cb("no password for user");
+      if (!o.existing_user) return cb("no password for user");
 
       exports.checkAuth(o.existing_user, function(err, hash) {
         cb(err, hash, o.existing_user);
@@ -337,8 +337,9 @@ exports.gotVerificationSecret = function(secret, cb) {
       if (err) {
         logUnexpectedError(err);
         cb(err);
-      } else if (rows.length === 0) cb("unknown secret");
-      else {
+      } else if (rows.length === 0) {
+        cb("unknown secret");
+      } else {
         var o = rows[0];
 
         // delete the record
diff --git a/lib/static_resources.js b/lib/static_resources.js
index 15fce2a8b3782857152b448697115ef8c3d19931..92f5c95cefa19b66e9206ed015cf9f95de18d371 100644
--- a/lib/static_resources.js
+++ b/lib/static_resources.js
@@ -80,6 +80,9 @@ var dialog_js = und.flatten([
     '/shared/history.js',
     '/shared/state_machine.js',
 
+    '/shared/models/models.js',
+    '/shared/models/interaction_data.js',
+
     '/shared/modules/interaction_data.js',
 
     '/dialog/resources/internal_api.js',
diff --git a/lib/wsapi/complete_email_addition.js b/lib/wsapi/complete_email_addition.js
index fcff7281387b49d78fb53a05ad4b74187dd36db2..7756fcf5b9cc8bb06cc14c7ba7768901854c6cad 100644
--- a/lib/wsapi/complete_email_addition.js
+++ b/lib/wsapi/complete_email_addition.js
@@ -21,13 +21,40 @@ exports.process = function(req, res) {
   //
   // 1. you must already be authenticated as the user who initiated the verification
   // 2. you must provide the password of the initiator.
-  //
+
+  // TRANSITIONAL CODE COMMENT
+  // for issue 1000 we moved initial password selection to the browserid dialog (from
+  // the verification page).  Rolling out this change causes some temporal pain.
+  // Outstannding verification links sent before the change was deployed will have
+  // email addition requests that require passwords without passwords in the stage table.
+  // When the verification page is loaded for
+  // these links, we prompt the user for a password.  That password is sent up with
+  // the request.  this code and comment should all be purged after the new code
+  // has been in production for 2 weeks.
+
+  var transitionalPassword = null;
+
+  // END TRANSITIONAL CODE COMMENT
+
+
   db.authForVerificationSecret(req.body.token, function(err, initiator_hash, initiator_uid) {
     if (err) {
       logger.info("unknown verification secret: " + err);
       return wsapi.databaseDown(res, err);
     }
 
+    // TRANSITIONAL CODE
+    if (!initiator_hash) {
+      if (!req.body.pass) return httputils.authRequired(res, "password required");
+      var err = wsapi.checkPassword(req.body.pass);
+      if (err) {
+        logger.warn("invalid password received: " + err);
+        return httputils.badRequest(res, err);
+      }
+      transitionalPassword = req.body.pass;
+      postAuthentication();
+    } else
+    // END TRANSITIONAL CODE
     if (req.session.userid === initiator_uid) {
       postAuthentication();
     } else if (typeof req.body.pass === 'string') {
@@ -53,6 +80,23 @@ exports.process = function(req, res) {
         } else {
           wsapi.authenticateSession(req.session, uid, 'password');
           res.json({ success: true });
+
+          // TRANSITIONAL CODE
+          if (transitionalPassword) {
+            wsapi.bcryptPassword(transitionalPassword, function(err, hash) {
+              if (err) {
+                logger.warn("couldn't bcrypt pass for old verification link: " + err);
+                return;
+              }
+
+              db.updatePassword(uid, hash, function(err) {
+                if (err) {
+                  logger.warn("couldn't bcrypt pass for old verification link: " + err);
+                }
+              });
+            });
+          }
+          // END TRANSITIONAL CODE
         }
       });
     };
diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js
index e507e0f95e1396268a4f180100f00191ee469656..5b87c3ec9af756562a37076fb88a2e9f09713f99 100644
--- a/lib/wsapi/complete_user_creation.js
+++ b/lib/wsapi/complete_user_creation.js
@@ -28,18 +28,36 @@ exports.process = function(req, res) {
   // and then control a browserid account that they can use to prove they own
   // the email address of the attacked.
 
+  // TRANSITIONAL CODE COMMENT
+  // for issue 1000 we moved initial password selection to the browserid dialog (from
+  // the verification page).  Rolling out this change causes some temporal pain.
+  // Outstannding verification links sent before the change was deployed will have
+  // new user requests without passwords.  When the verification page is loaded for
+  // these links, we prompt the user for a password.  That password is sent up with
+  // the request.  this code and comment should all be purged after the new code
+  // has been in production for 2 weeks.
+  // END TRANSITIONAL CODE COMMENT
+
   // is this the same browser?
   if (typeof req.session.pendingCreation === 'string' &&
       req.body.token === req.session.pendingCreation) {
-    postAuthentication();
+    return postAuthentication();
   }
   // is a password provided?
   else if (typeof req.body.pass === 'string') {
     return db.authForVerificationSecret(req.body.token, function(err, hash) {
+      // TRANSITIONAL CODE
+      // if hash is null, no password was provided during verification and
+      // this is an old-style verification.  We accept the password and will
+      // update it after the verification is complete.
+      if (err == 'no password for user' || !hash) return postAuthentication();
+      // END TRANSITIONAL CODE
+
       if (err) {
         logger.warn("couldn't get password for verification secret: " + err);
         return wsapi.databaseDown(res, err);
       }
+
       bcrypt.compare(req.body.pass, hash, function (err, success) {
         if (err) {
           logger.warn("max load hit, failing on auth request with 503: " + err);
@@ -47,7 +65,7 @@ exports.process = function(req, res) {
         } else if (!success) {
           return httputils.authRequired(res, "password mismatch");
         } else {
-          postAuthentication();
+          return postAuthentication();
         }
       });
     });
@@ -65,19 +83,58 @@ exports.process = function(req, res) {
 
       if (!known) return res.json({ success: false} );
 
-      db.gotVerificationSecret(req.body.token, function(err, email, uid) {
-        if (err) {
-          logger.warn("couldn't complete email verification: " + err);
-          wsapi.databaseDown(res, err);
-        } else {
-          // FIXME: not sure if we want to do this (ba)
-          // at this point the user has set a password associated with an email address
-          // that they've verified.  We create an authenticated session.
-          wsapi.authenticateSession(req.session, uid, 'password',
-                                    config.get('ephemeral_session_duration_ms'));
-          res.json({ success: true });
+      // TRANSITIONAL CODE
+      // user is authorized (1 or 2 above) OR user has no password set, in which
+      // case for a short time we'll accept the password provided with the verification
+      // link, and set it as theirs.
+      var transitionalPassword = null;
+
+      db.authForVerificationSecret(req.body.token, function(err, hash) {
+        if (err == 'no password for user' || !hash) {
+          if (!req.body.pass) return httputils.authRequired(res, "password required");
+          err = wsapi.checkPassword(req.body.pass);
+          if (err) {
+            logger.warn("invalid password received: " + err);
+            return httputils.badRequest(res, err);
+          }
+          transitionalPassword = req.body.pass;
         }
+        completeCreation();
       });
+      // END TRANSITIONAL CODE
+
+      function completeCreation() {
+        db.gotVerificationSecret(req.body.token, function(err, email, uid) {
+          if (err) {
+            logger.warn("couldn't complete email verification: " + err);
+            wsapi.databaseDown(res, err);
+          } else {
+            // FIXME: not sure if we want to do this (ba)
+            // at this point the user has set a password associated with an email address
+            // that they've verified.  We create an authenticated session.
+            wsapi.authenticateSession(req.session, uid, 'password',
+                                      config.get('ephemeral_session_duration_ms'));
+            res.json({ success: true });
+
+            // TRANSITIONAL CODE
+            if (transitionalPassword) {
+              wsapi.bcryptPassword(transitionalPassword, function(err, hash) {
+                if (err) {
+                  logger.warn("couldn't bcrypt pass for old verification link: " + err);
+                  return;
+                }
+
+                db.updatePassword(uid, hash, function(err) {
+                  if (err) {
+                    logger.warn("couldn't bcrypt pass for old verification link: " + err);
+                  }
+                });
+              });
+            }
+            // END TRANSITIONAL CODE
+          }
+        });
+      }
     });
   }
 };
diff --git a/lib/wsapi/email_for_token.js b/lib/wsapi/email_for_token.js
index f492bcff595e0978c7f96e24f31b287fdceb8851..ac0a5d9543514ac4cb74c1ab3986eded0ad63421 100644
--- a/lib/wsapi/email_for_token.js
+++ b/lib/wsapi/email_for_token.js
@@ -19,7 +19,7 @@ exports.args = ['token'];
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  db.emailForVerificationSecret(req.query.token, function(err, email, uid) {
+  db.emailForVerificationSecret(req.query.token, function(err, email, uid, hash) {
     if (err) {
       if (err === 'database unavailable') {
         httputils.serviceUnavailable(res, err);
@@ -30,24 +30,64 @@ exports.process = function(req, res) {
         });
       }
     } else {
-      // must the user authenticate?  This is true if they are not authenticated
-      // as the uid who initiated the verification, and they are not on the same
-      // browser as the initiator
-      var must_auth = true;
+      function checkMustAuth() {
+        // must the user authenticate?  This is true if they are not authenticated
+        // as the uid who initiated the verification, and they are not on the same
+        // browser as the initiator
+        var must_auth = true;
 
-      if (uid && req.session.userid === uid) {
-        must_auth = false;
+        if (uid && req.session.userid === uid) {
+          must_auth = false;
+        }
+        else if (!uid && typeof req.session.pendingCreation === 'string' &&
+                 req.query.token === req.session.pendingCreation) {
+          must_auth = false;
+        }
+
+        res.json({
+          success: true,
+          email: email,
+          must_auth: must_auth
+        });
+      }
+
+      // backwards compatibility - issue #1592
+      // if there is no password in the user record, and no password in the staged
+      // table, then we require a password be fetched from the user upon verification.
+      // these checks are temporary and should disappear in 1 trains time.
+      function needsPassword() {
+        // no password is set neither in the user table nor in the staged record.
+        // the user must pick a password
+        res.json({
+          success: true,
+          email: email,
+          needs_password: true
+        });
       }
-      else if (!uid && typeof req.session.pendingCreation === 'string' &&
-               req.query.token === req.session.pendingCreation) {
-        must_auth = false;
+
+      if (!hash) {
+        if (!uid) {
+          needsPassword();
+        } else {
+          db.checkAuth(uid, function(err, hash) {
+            if (err) {
+              return res.json({
+                success: false,
+                reason: err
+              });
+            }
+
+            if (!hash) {
+              needsPassword();
+            } else {
+              checkMustAuth();
+            }
+          });
+        }
+      } else {
+        checkMustAuth();
       }
 
-      res.json({
-        success: true,
-        email: email,
-        must_auth: must_auth
-      });
     }
   });
 };
diff --git a/resources/static/css/style.css b/resources/static/css/style.css
index 86b0b219f6f3b529e0ba677130ce4f0d0271ad76..802295daca104265ab0c2bf4feb716cb39357683 100644
--- a/resources/static/css/style.css
+++ b/resources/static/css/style.css
@@ -669,7 +669,7 @@ h1 {
   margin-bottom: 10px;
 }
 
-.siteinfo, #congrats, .password_entry, .enter_password .hint, #unknown_secondary, #primary_verify, .verify_primary .submit {
+.siteinfo, #congrats, .password_entry, #verify_password, .enter_password .hint, #unknown_secondary, #primary_verify, .verify_primary .submit {
   display: none;
 }
 
@@ -677,7 +677,7 @@ h1 {
   float: left;
 }
 
-.enter_password .password_entry, .known_secondary .password_entry,
+.enter_password .password_entry, .enter_verify_password #verify_password, .known_secondary .password_entry,
 .unknown_secondary #unknown_secondary, .verify_primary #verify_primary {
   display: block;
 }
diff --git a/resources/static/pages/verify_secondary_address.js b/resources/static/pages/verify_secondary_address.js
index 2ccfb0c804ec90c672114c6235fcbefee8b3e0df..607a5621c80d4b477de7ea572c4008b007d6a30a 100644
--- a/resources/static/pages/verify_secondary_address.js
+++ b/resources/static/pages/verify_secondary_address.js
@@ -17,6 +17,7 @@ BrowserID.verifySecondaryAddress = (function() {
       validation = bid.Validation,
       token,
       sc,
+      needsPassword,
       mustAuth,
       verifyFunction;
 
@@ -36,7 +37,11 @@ BrowserID.verifySecondaryAddress = (function() {
 
   function submit(oncomplete) {
     var pass = dom.getInner("#password") || undefined,
-        valid = !mustAuth || validation.password(pass);
+        vpass = dom.getInner("#vpassword") || undefined,
+        valid = (!needsPassword ||
+                    validation.passwordAndValidationPassword(pass, vpass))
+             && (!mustAuth ||
+                    validation.password(pass));
 
     if (valid) {
       user[verifyFunction](token, pass, function(info) {
@@ -56,13 +61,25 @@ BrowserID.verifySecondaryAddress = (function() {
       if(info) {
         showRegistrationInfo(info);
 
+        needsPassword = info.needs_password;
         mustAuth = info.must_auth;
 
-        if (mustAuth) {
+        if (needsPassword) {
+          // This is a fix for legacy users who started the user creation
+          // process without setting their password in the dialog.  If the user
+          // needs a password, they must set it now.  Once all legacy users are
+          // verified or their links invalidated, this flow can be removed.
+          dom.addClass("body", "enter_password");
+          dom.addClass("body", "enter_verify_password");
+          complete(oncomplete, true);
+        }
+        else if (mustAuth) {
+          // These are users who have set their passwords inside of the dialog.
           dom.addClass("body", "enter_password");
           complete(oncomplete, true);
         }
         else {
+          // These are users who do not have to set their passwords at all.
           submit(oncomplete);
         }
       }
diff --git a/resources/static/shared/models/interaction_data.js b/resources/static/shared/models/interaction_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..d5ad94544acbc97e3712c26c0025caa6eed3cd3f
--- /dev/null
+++ b/resources/static/shared/models/interaction_data.js
@@ -0,0 +1,163 @@
+/*globals BrowserID: true */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+BrowserID.Models.InteractionData = (function() {
+  "use strict";
+
+  var bid = BrowserID,
+      storage = bid.getStorage(),
+      network = bid.Network,
+      complete = bid.Helpers.complete;
+
+  function getInteractionData() {
+    var interactionData;
+    try {
+      interactionData = JSON.parse(storage.interaction_data);
+    } catch(e) {
+    }
+
+    return interactionData || {};
+  }
+
+  function setInteractionData(data) {
+    try {
+      storage.interaction_data = JSON.stringify(data);
+    } catch(e) {
+      storage.removeItem("interaction_data");
+    }
+  }
+
+  function push(newData) {
+    stageCurrent();
+
+    var interactionData = getInteractionData();
+    interactionData.current = newData;
+
+    setInteractionData(interactionData);
+  }
+
+  function getCurrent() {
+    var interactionData = getInteractionData();
+
+    return interactionData.current;
+  }
+
+  function setCurrent(data) {
+    var interactionData = getInteractionData();
+    interactionData.current = data;
+    setInteractionData(interactionData);
+  }
+
+  function stageCurrent() {
+    // Push existing current data to the staged list.  This allows
+    // us to get/clear the staged list without affecting the current data.
+    var interactionData = getInteractionData();
+
+    if (interactionData.current) {
+      var staged = interactionData.staged = interactionData.staged || [];
+      staged.unshift(interactionData.current);
+
+      delete interactionData.current;
+
+      setInteractionData(interactionData);
+    }
+  }
+
+  function getStaged() {
+    var interactionData = getInteractionData();
+    return interactionData.staged || [];
+  }
+
+  function clearStaged() {
+    var interactionData = getInteractionData();
+    delete interactionData.staged;
+    setInteractionData(interactionData);
+  }
+
+  // We'll try to publish past interaction data to the server if it exists.
+  // The psuedo transactional model employed here is to attempt to post, and
+  // only once we receive a server response do we purge data.  We don't
+  // care if the post is a success or failure as this data is not
+  // critical to the functioning of the system (and some failure scenarios
+  // simply won't resolve with retries - like corrupt data, or too much
+  // data)
+  function publishStaged(oncomplete) {
+     var data = getStaged();
+
+    // XXX: should we even try to post data if it's larger than some reasonable
+    // threshold?
+    if (data && data.length !== 0) {
+      network.sendInteractionData(data, function() {
+        clearStaged();
+        complete(oncomplete, true);
+      }, function(status) {
+        // if the server returns a 413 error, (too much data posted), then
+        // let's clear our local storage and move on.  This does mean we
+        // loose some interaction data, but it shouldn't be statistically
+        // significant.
+        if (status && status.network && status.network.status === 413) {
+          clearStaged();
+        }
+        complete(oncomplete, false);
+      });
+    }
+    else {
+      complete(oncomplete, false);
+    }
+  }
+
+  return {
+    /**
+     * add a new interaction blob to localstorage, this will *push* any stored
+     * blobs to the 'staged' backlog, and happens when a new dialog interaction
+     * begins.
+     * @method push
+     * @param {object} data - an object to push onto the queue
+     * @returns nada
+     */
+    push: push,
+    /**
+     * read the interaction data blob associated with the current interaction
+     * @method getCurrent
+     * @returns a JSON object containing the latest interaction data blob
+     */
+    getCurrent: getCurrent,
+    /**
+     * overwrite the interaction data blob associated with the current interaction
+     * @method setCurrent
+     * @param {object} data - the object to overwrite current with
+     */
+    setCurrent: setCurrent,
+    /**
+     * Shift any "current" data into the staged list.  No data will be listed
+     * as current afterwards.
+     * @method stageCurrent
+     */
+    stageCurrent: stageCurrent,
+    /**
+     * get all past saved interaction data (returned as a JSON array), excluding
+     * the "current" data (that which is being collected now).
+     * @method getStaged
+     * @returns an array, possibly of length zero if no past interaction data is
+     * available
+     */
+    getStaged: getStaged,
+    /**
+     * publish staged data. Staged data will be cleared if successfully posted
+     * to server or if server returns 413 - too much data.
+     * @param {function} [oncomplete] - function to call when complete.  Called
+     * with true if data was successfully sent to server, false otw.
+     * @method publishStaged
+     */
+    publishStaged: publishStaged,
+    /**
+     * clear all interaction data, except the current, in-progress
+     * collection.
+     * @method clearStaged()
+     */
+    clearStaged: clearStaged
+  };
+
+}());
diff --git a/resources/static/shared/models/models.js b/resources/static/shared/models/models.js
new file mode 100644
index 0000000000000000000000000000000000000000..845cc04763349d29296935b5148f22f7f37f5032
--- /dev/null
+++ b/resources/static/shared/models/models.js
@@ -0,0 +1,7 @@
+/*globals BrowserID: true */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+BrowserID.Models = {};
+
diff --git a/resources/static/shared/modules/interaction_data.js b/resources/static/shared/modules/interaction_data.js
index 1ad832138c3305ed70db299a482483b8f54efb41..0ef57fd67381c5180d8e61a0734124032d6bf1bd 100644
--- a/resources/static/shared/modules/interaction_data.js
+++ b/resources/static/shared/modules/interaction_data.js
@@ -24,7 +24,7 @@
 
 BrowserID.Modules.InteractionData = (function() {
   var bid = BrowserID,
-      storage = bid.Storage.interactionData,
+      model = bid.Models.InteractionData,
       network = bid.Network,
       complete = bid.Helpers.complete,
       dom = bid.DOM,
@@ -41,7 +41,9 @@ BrowserID.Modules.InteractionData = (function() {
     // session data must be published independently of whether the current
     // dialog session is allowed to sample data. This is because the original
     // dialog session has already decided whether to collect data.
-    publishStored();
+
+    model.stageCurrent();
+    publishStored.call(self);
 
     // set the sample rate as defined by the server.  It's a value
     // between 0..1, integer or float, and it specifies the percentage
@@ -78,63 +80,63 @@ BrowserID.Modules.InteractionData = (function() {
     // as soon as the first session_context completes for the next dialog
     // session.  Use a push because old data *may not* have been correctly
     // published to a down server or erroring web service.
-    storage.push(currentData);
+    model.push(currentData);
 
     self.initialEventStream = null;
 
     self.samplesBeingStored = true;
   }
 
-  // At every load, after session_context returns, we'll try to publish
-  // past interaction data to the server if it exists.  The psuedo
-  // transactional model employed here is to attempt to post, and only
-  // once we receive a server response do we purge data.  We don't
-  // care if the post is a success or failure as this data is not
-  // critical to the functioning of the system (and some failure scenarios
-  // simply won't resolve with retries - like corrupt data, or too much
-  // data)
+  // At every load, after session_context returns, try to publish the previous
+  // data.  We have to wait until session_context completes so that we have
+  // a csrf token to send.
   function publishStored(oncomplete) {
-    var data = storage.get();
-
-    // XXX: should we even try to post data if it's larger than some reasonable
-    // threshold?
-    if (data && data.length !== 0) {
-      network.sendInteractionData(data, function() {
-        storage.clear();
-        complete(oncomplete, true);
-      }, function(status) {
-        // if the server returns a 413 error, (too much data posted), then
-        // let's clear our local storage and move on.  This does mean we
-        // loose some interaction data, but it shouldn't be statistically
-        // significant.
-        if (status && status.network && status.network.status === 413) {
-          storage.clear();
-        }
-        complete(oncomplete, false);
-      });
-    }
-    else {
-      complete(oncomplete, false);
-    }
+    var self=this;
+
+    model.publishStaged(function(status) {
+      var msg = status ? "interaction_data_send_complete" : "interaction_data_send_error";
+      self.publish(msg);
+      complete(oncomplete, status);
+    });
   }
 
 
   function addEvent(eventName) {
     var self=this;
-
     if (self.samplingEnabled === false) return;
 
     var eventData = [ eventName, new Date() - self.startTime ];
     if (self.samplesBeingStored) {
-      var d = storage.current() || {};
+      var d = model.getCurrent() || {};
       if (!d.event_stream) d.event_stream = [];
       d.event_stream.push(eventData);
-      storage.setCurrent(d);
+      model.setCurrent(d);
     } else {
       self.initialEventStream.push(eventData);
     }
   }
 
+  function getCurrent() {
+    var self=this;
+    if(self.samplingEnabled === false) return;
+
+    if (self.samplesBeingStored) {
+      return model.getCurrent();
+    }
+  }
+
+  function getCurrentEventStream() {
+    var self=this;
+    if(self.samplingEnabled === false) return;
+
+    if (self.samplesBeingStored) {
+      return model.getCurrent().event_stream;
+    }
+    else {
+      return self.initialEventStream;
+    }
+  }
+
   var Module = bid.Modules.PageModule.extend({
     start: function(options) {
       options = options || {};
@@ -147,29 +149,28 @@ BrowserID.Modules.InteractionData = (function() {
       // a continuation, samplingEnabled will be decided on the first "
       // context_info" event, which corresponds to the first time
       // 'session_context' returns from the server.
+      // samplingEnabled flag ignored for a continuation.
       self.samplingEnabled = options.samplingEnabled;
 
       // continuation means the users dialog session is continuing, probably
       // due to a redirect to an IdP and then a return after authentication.
       if (options.continuation) {
-        var previousData = storage.current();
-
-        var samplingEnabled = self.samplingEnabled = !!previousData.event_stream;
-        if (samplingEnabled) {
+        // There will be no current data if the previous session was not
+        // allowed to save.
+        var previousData = model.getCurrent();
+        if (previousData) {
           self.startTime = Date.parse(previousData.local_timestamp);
 
-          if (typeof self.samplingEnabled === "undefined") {
-            self.samplingEnabled = samplingEnabled;
-          }
 
           // instead of waiting for session_context to start appending data to
           // localStorage, start saving into localStorage now.
-          self.samplesBeingStored = true;
+          self.samplingEnabled = self.samplesBeingStored = true;
         }
         else {
-          // If there was no previous event stream, that means data collection
+          // If there was no previous data, that means data collection
           // was not allowed for the previous session.  Return with no further
           // action, data collection is not allowed for this session either.
+          self.samplingEnabled = false;
           return;
         }
       }
@@ -179,7 +180,7 @@ BrowserID.Modules.InteractionData = (function() {
         // The initialEventStream is used to store events until onSessionContext
         // is called.  Once onSessionContext is called and it is known whether
         // the user's data will be saved, initialEventStream will either be
-        // discarded or added to the data set that is saved to localStorage.
+        // discarded or added to the data set that is saved to localmodel.
         self.initialEventStream = [];
         self.samplesBeingStored = false;
 
@@ -194,16 +195,8 @@ BrowserID.Modules.InteractionData = (function() {
     },
 
     addEvent: addEvent,
-
-    getCurrentStoredData: function() {
-      var und;
-      return this.samplesBeingStored ? storage.current() : und;
-    },
-
-    getEventStream: function() {
-      return this.samplesBeingStored ? storage.current().event_stream : this.initialEventStream || [];
-    },
-
+    getCurrent: getCurrent,
+    getCurrentEventStream: getCurrentEventStream,
     publishStored: publishStored
   });
 
diff --git a/resources/static/shared/storage.js b/resources/static/shared/storage.js
index 82ed0be6d3838d2d4e93695530005162997e38e6..1fa52161a9da6e0b65c7c0bad02e79dfe9682800 100644
--- a/resources/static/shared/storage.js
+++ b/resources/static/shared/storage.js
@@ -3,13 +3,8 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*globals BrowserID: true, console: true */
-
-BrowserID.Storage = (function() {
-  "use strict";
-
-  var jwcrypto,
-      ONE_DAY_IN_MS = (1000 * 60 * 60 * 24),
-      storage;
+BrowserID.getStorage = function() {
+  var storage;
 
   try {
     storage = localStorage;
@@ -27,6 +22,16 @@ BrowserID.Storage = (function() {
     };
   }
 
+  return storage;
+};
+
+BrowserID.Storage = (function() {
+  "use strict";
+
+  var jwcrypto,
+      ONE_DAY_IN_MS = (1000 * 60 * 60 * 24),
+      storage = BrowserID.getStorage();
+
   // temporary, replace with helpers.log if storage uses elog long term...
   function elog (msg) {
     if (window.console && console.error) console.error(msg);
@@ -401,56 +406,6 @@ BrowserID.Storage = (function() {
     storage.emailToUserID = JSON.stringify(allInfo);
   }
 
-  function pushInteractionData(data) {
-    var id;
-    try {
-      id = JSON.parse(storage.interactionData);
-      id.unshift(data);
-    } catch(e) {
-      id = [ data ];
-    }
-    storage.interactionData = JSON.stringify(id);
-  }
-
-  function currentInteractionData() {
-    try {
-      return storage.interactionData ? JSON.parse(storage.interactionData)[0] : {};
-    } catch(e) {
-      elog(e);
-      return {};
-    }
-  }
-
-  function setCurrentInteractionData(data) {
-    var id;
-    try {
-      id = JSON.parse(storage.interactionData);
-      id[0] = data;
-    } catch(e) {
-      elog(e);
-      id = [ data ];
-    }
-    storage.interactionData = JSON.stringify(id);
-  }
-
-  function getAllInteractionData() {
-    try {
-      return storage.interactionData ? JSON.parse(storage.interactionData) : [];
-    } catch(e) {
-      if (window.console && console.error) console.error(e);
-      return [];
-    }
-  }
-
-  function clearInteractionData() {
-    try {
-      storage.interactionData = JSON.stringify([]);
-    } catch(e) {
-      storage.removeItem("interactionData");
-      elog(e);
-    }
-  }
-
   return {
     /**
      * Add an email address and optional key pair.
@@ -533,44 +488,6 @@ BrowserID.Storage = (function() {
       remove: generic2KeyRemove.curry("main_site", "signInEmail")
     },
 
-    interactionData: {
-      /**
-       * add a new interaction blob to localstorage, this will *push* any stored
-       * blobs to the 'completed' backlog, and happens when a new dialog interaction
-       * begins.
-       * @param {object} data - an object to push onto the queue
-       * @method interactionData.push()
-       * @returns nada
-       */
-      push: pushInteractionData,
-      /**
-       * read the interaction data blob associated with the current interaction
-       * @method interactionData.current()
-       * @returns a JSON object containing the latest interaction data blob
-       */
-      current: currentInteractionData,
-      /**
-       * overwrite the interaction data blob associated with the current interaction
-       * @param {object} data - the object to overwrite current with
-       * @method interactionData.setCurrent()
-       */
-      setCurrent: setCurrentInteractionData,
-      /**
-       * get all past saved interaction data (returned as a JSON array), excluding
-       * the "current" data (that which is being collected now).
-       * @method interactionData.get()
-       * @returns an array, possibly of length zero if no past interaction data is
-       * available
-       */
-      get: getAllInteractionData,
-      /**
-       * clear all interaction data, except the current, in-progress
-       * collection.
-       * @method interactionData.clear()
-       */
-      clear: clearInteractionData
-    },
-
     usersComputer: {
       /**
        * Query whether the user has confirmed that this is their computer
diff --git a/resources/static/test/cases/pages/verify_secondary_address.js b/resources/static/test/cases/pages/verify_secondary_address.js
index a770237db60734b31e39a6d9f82553bb917b9bc5..d7eacaa574bb8d489c7a5897d0913c2060d43a6b 100644
--- a/resources/static/test/cases/pages/verify_secondary_address.js
+++ b/resources/static/test/cases/pages/verify_secondary_address.js
@@ -116,6 +116,7 @@
     xhr.useResult("mustAuth");
     createController(config, function() {
       xhr.useResult("valid");
+      testHasClass("body", "enter_password");
       controller.submit(function(status) {
         equal(status, true, "correct status");
         testHasClass("body", "complete");
@@ -134,4 +135,88 @@
     });
   });
 
+  asyncTest("must set password, successful login", function() {
+    xhr.useResult("needsPassword");
+    createController(config, function() {
+      xhr.useResult("valid");
+
+      $("#password").val("password");
+      $("#vpassword").val("password");
+
+      testHasClass("body", "enter_password");
+      testHasClass("body", "enter_verify_password");
+
+      controller.submit(function(status) {
+        equal(status, true, "correct status");
+        testHasClass("body", "complete");
+        start();
+      });
+    });
+  });
+
+  asyncTest("must set password, too short a password", function() {
+    xhr.useResult("needsPassword");
+    createController(config, function() {
+      xhr.useResult("valid");
+
+      $("#password").val("pass");
+      $("#vpassword").val("pass");
+
+      controller.submit(function(status) {
+        equal(status, false, "correct status");
+        testHelpers.testTooltipVisible();
+        start();
+      });
+    });
+  });
+
+  asyncTest("must set password, too long a password", function() {
+    xhr.useResult("needsPassword");
+    createController(config, function() {
+      xhr.useResult("valid");
+
+      var pass = testHelpers.generateString(81);
+      $("#password").val(pass);
+      $("#vpassword").val(pass);
+
+      controller.submit(function(status) {
+        equal(status, false, "correct status");
+        testHelpers.testTooltipVisible();
+        start();
+      });
+    });
+  });
+
+  asyncTest("must set password, missing verification password", function() {
+    xhr.useResult("needsPassword");
+    createController(config, function() {
+      xhr.useResult("valid");
+
+      $("#password").val("password");
+      $("#vpassword").val("");
+
+      controller.submit(function(status) {
+        equal(status, false, "correct status");
+        testHelpers.testTooltipVisible();
+        start();
+      });
+    });
+  });
+
+  asyncTest("must set password, mismatched passwords", function() {
+    xhr.useResult("needsPassword");
+    createController(config, function() {
+      xhr.useResult("valid");
+
+      $("#password").val("password");
+      $("#vpassword").val("password1");
+
+      controller.submit(function(status) {
+        equal(status, false, "correct status");
+        testHelpers.testTooltipVisible();
+        start();
+      });
+    });
+  });
+
 }());
diff --git a/resources/static/test/cases/shared/models/interaction_data.js b/resources/static/test/cases/shared/models/interaction_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..5454ccfc6e88f1e0a8cd1e250e132521d50192ff
--- /dev/null
+++ b/resources/static/test/cases/shared/models/interaction_data.js
@@ -0,0 +1,108 @@
+
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+(function() {
+  var bid = BrowserID,
+      model = bid.Models.InteractionData,
+      testHelpers = bid.TestHelpers,
+      testObjectValuesEqual = testHelpers.testObjectValuesEqual,
+      xhr = bid.Mocks.xhr;
+
+  module("shared/models/interaction_data", {
+    setup: function() {
+      testHelpers.setup();
+      localStorage.removeItem("interaction_data");
+    },
+
+    teardown: function() {
+      testHelpers.teardown();
+    }
+  });
+
+  test("after push, most recently pushed data available through getCurrent, getStaged gets previous data sets", function() {
+    model.push({ foo: "bar" });
+    equal(model.getCurrent().foo, "bar",
+          "after pushing new interaction data, it's returned from .getCurrent()");
+
+    equal(model.getStaged().length, 0, "no data is yet staged");
+
+    model.push({ foo: "baz" });
+
+    equal(model.getCurrent().foo, "baz", "current points to new data set")
+    var staged = model.getStaged();
+
+    equal(staged.length, 1, "only one staged item");
+    testObjectValuesEqual(staged[0], { foo: "bar" });
+  });
+
+  test("setCurrent data overwrites current", function() {
+    model.clearStaged();
+    model.push({ foo: "bar" });
+    model.setCurrent({ foo: "baz" });
+    equal(model.getCurrent().foo, "baz",
+          "overwriting current interaction data works");
+  });
+
+  test("clearStaged clears staged interaction data but leaves current data unaffected", function() {
+    model.push({ foo: "bar" });
+    model.push({ foo: "baz" });
+    model.clearStaged();
+    equal(model.getStaged().length, 0,
+          "after clearStageding, interaction data is zero length");
+    equal(model.getCurrent().foo, "baz",
+          "after clearStageding, current data is unaffected");
+  });
+
+  test("stageCurrent - stage the current data, if any. no data is current afterwards", function() {
+    // There is no current data to stage.
+    model.stageCurrent();
+    equal(model.getStaged().length, 0, "no data to staged");
+
+    model.push({ foo: "bar" });
+    model.stageCurrent();
+
+    equal(model.getStaged().length, 1, "current data staged");
+    equal(typeof model.getCurrent(), "undefined", "current data removed after being staged");
+  });
+
+  asyncTest("publishStored - publish any staged data", function() {
+    // There is no currently staged data.
+    model.publishStaged(function(status) {
+      equal(status, false, "no data currently staged");
+
+      // Simulate a throttling
+      // desired result - data is purged from staging table
+
+      // The first pushed data will become staged.
+      model.push({ foo: "bar" });
+      model.stageCurrent();
+
+      xhr.useResult("throttle");
+      model.publishStaged(function(status) {
+        equal(false, status, "data throttling returns false status");
+        // the previously staged data should we wiped on a throttling response.
+
+        // When the interaction_data next completes, this will be the only data
+        // that is pushed.
+        model.push({ foo: "baz" });
+        model.stageCurrent();
+
+        xhr.useResult("valid");
+        model.publishStaged(function(status) {
+          equal(true, status, "data successfully posted");
+          var request = xhr.getLastRequest('/wsapi/interaction_data'),
+              previousSessionsData = JSON.parse(request.data).data;
+
+          equal(previousSessionsData.length, 1, "sending correct result sets");
+          equal(previousSessionsData[0].foo, "baz", "correct data sent");
+          start();
+        });
+      });
+
+    });
+
+  });
+}());
diff --git a/resources/static/test/cases/shared/modules/interaction_data.js b/resources/static/test/cases/shared/modules/interaction_data.js
index 951c506903538a898a6e0c80c8fa8913aeff3593..c3daf5632d88f963af41f9332389fb3a8f710986 100644
--- a/resources/static/test/cases/shared/modules/interaction_data.js
+++ b/resources/static/test/cases/shared/modules/interaction_data.js
@@ -9,11 +9,16 @@
   var bid = BrowserID,
       testHelpers = bid.TestHelpers,
       network = bid.Network,
-      storage = bid.Storage,
+      model = bid.Models.InteractionData,
+      xhr = bid.Mocks.xhr,
+      mediator = bid.Mediator,
       controller;
 
   module("shared/modules/interaction_data", {
-    setup: testHelpers.setup,
+    setup: function() {
+      testHelpers.setup();
+      localStorage.removeItem("interaction_data");
+    },
     teardown: function() {
       testHelpers.teardown();
 
@@ -36,25 +41,35 @@
   }
 
   asyncTest("samplingEnabled - ensure data collection working as expected", function() {
+    // Desired sequence:
+    // 1. When session_context completes, initialize this session's interaction
+    // data, sends previous session's data.
+    // 2. when network.sendInteractionData completes, previous session's data is
+    // erased, current session's data is unaffected.
+
+    // simulate data stored for last session
+    model.push({ timestamp: new Date().getTime() });
+
     createController();
 
     controller.addEvent("before_session_context");
 
-    var events = controller.getEventStream();
+    var events = controller.getCurrentEventStream();
     ok(indexOfEvent(events, "before_session_context") > -1, "before_session_context correctly saved to event stream");
-    ok(indexOfEvent(events, "after_session_context") === -1, "after_session_context not yet added to current event stream");
 
-    // with context initializes the current stored data.
-    network.withContext(function() {
-      var data = controller.getCurrentStoredData();
+    // Add an XHR delay to simulate interaction_data completeing after
+    // session_context completes.
+    xhr.setDelay(5);
+
+    mediator.subscribe("interaction_data_send_complete", function() {
+      var data = controller.getCurrent();
 
       // Make sure expected items are in the current stored data.
       testHelpers.testKeysInObject(data, ["event_stream", "sample_rate", "timestamp", "lang"]);
 
       controller.addEvent("after_session_context");
 
-      var events = controller.getEventStream();
-
+      events = controller.getCurrentEventStream();
       // Make sure both the before_session_context and after_session_context
       // are both on the event stream.
       ok(indexOfEvent(events, "before_session_context") > -1, "before_session_context correctly saved to current event stream");
@@ -73,28 +88,7 @@
       start();
     });
 
-  });
-
-  asyncTest("publish data", function() {
-    createController();
-
-    // force saved data to be cleared.
-    storage.interactionData.clear();
-    controller.publishStored(function(status) {
-      equal(status, false, "no data to publish");
-
-      // session context is required start saving events to localStorage.
-      network.withContext(function() {
-
-        // Add an event which should allow us to publish
-        controller.addEvent("something_special");
-        controller.publishStored(function(status) {
-          equal(status, true, "data correctly published");
-
-          start();
-        });
-      });
-    });
+    network.withContext();
   });
 
   asyncTest("samplingEnabled set to false - no data collection occurs", function() {
@@ -104,12 +98,9 @@
     // no stored data.
     network.withContext(function() {
       controller.addEvent("after_session_context");
-      var events = controller.getEventStream();
-
-      var index = indexOfEvent(events, "after_session_context");
-      equal(index, -1, "events not being stored");
 
-      equal(typeof controller.getCurrentStoredData(), "undefined", "no stored data");
+      equal(typeof controller.getCurrent(), "undefined", "no stored data");
+      equal(typeof controller.getCurrentEventStream(), "undefined", "no data stored");
 
       controller.publishStored(function(status) {
         equal(status, false, "there was no data to publish");
@@ -135,7 +126,7 @@
       network.withContext(function() {
         controller.addEvent("session2_after_session_context");
 
-        var events = controller.getEventStream();
+        var events = controller.getCurrentEventStream();
 
         ok(indexOfEvent(events, "session1_before_session_context") > -1, "session1_before_session_context correctly saved to current event stream");
         ok(indexOfEvent(events, "session1_after_session_context") > -1, "session1_after_session_context correctly saved to current event stream");
@@ -166,12 +157,8 @@
       network.withContext(function() {
         controller.addEvent("session2_after_session_context");
 
-        var events = controller.getEventStream();
-
-        ok(indexOfEvent(events, "session1_before_session_context") === -1, "no data collected");
-        ok(indexOfEvent(events, "session1_after_session_context") === -1, "no data collected");
-        ok(indexOfEvent(events, "session2_before_session_context") === -1, "no data collected");
-        ok(indexOfEvent(events, "session2_after_session_context") === -1, "no data collected");
+        equal(typeof controller.getCurrent(), "undefined", "no data collected");
+        equal(typeof controller.getCurrentEventStream(), "undefined", "no data collected");
 
         controller.publishStored(function(status) {
           equal(status, false, "there was no data to publish");
@@ -179,8 +166,43 @@
         });
       });
     });
-
   });
 
 
+  asyncTest("simulate failed starts - data not sent until second successful session_context", function() {
+    // simulate three dialogs being opened.
+    // The first open dialog does not complete session_context, so data is
+    // never collected/sent for this session.
+    // The second has session_context complete, it starts collecting data which
+    // is sent when the third dialog has its session_context complete.
+    // The third has session_context complete and sends data for the second
+    // dialog opening.
+
+
+    // First open dialog never has session_context complete. Data is not
+    // collected.
+    createController();
+    controller.addEvent("session1_before_session_context");
+
+    // Second open dialog is the first to successfully complete
+    // session_context, data should be collected.
+    createController();
+    controller.addEvent("session2_before_session_context");
+    network.withContext(function() {
+
+      // Third open dialog successfully completes session_context, should send
+      // data for the 2nd open dialog once session_context completes.
+      createController();
+      controller.addEvent("session2_before_session_context");
+
+      network.withContext(function() {
+        var request = xhr.getLastRequest('/wsapi/interaction_data'),
+            previousSessionsData = JSON.parse(request.data).data;
+
+        equal(previousSessionsData.length, 1, "sending correct result sets");
+        start();
+      });
+    });
+  });
+
 }());
diff --git a/resources/static/test/cases/shared/storage.js b/resources/static/test/cases/shared/storage.js
index b9b230b5e362aacd4174960d173810eabea5faa7..c9406c84a21ef077b4a150f956a3666a0eccc505 100644
--- a/resources/static/test/cases/shared/storage.js
+++ b/resources/static/test/cases/shared/storage.js
@@ -179,37 +179,5 @@
     equal(typeof storage.signInEmail.get(), "undefined", "after remove, signInEmail is empty");
   });
 
-  test("push interaction data and get current", function() {
-    storage.interactionData.push({ foo: "bar" });
-    equal(storage.interactionData.current().foo, "bar",
-          "after pushing new interaction data, it's returned from .current()");
-  });
-
-  test("set interaction data overwrites current", function() {
-    storage.interactionData.clear();
-    storage.interactionData.push({ foo: "bar" });
-    storage.interactionData.setCurrent({ foo: "baz" });
-    equal(storage.interactionData.current().foo, "baz",
-          "overwriting current interaction data works");
-    equal(storage.interactionData.get().length, 1,
-          "overwriting doesn't append");
-  });
-
-  test("clear interaction data", function() {
-    storage.interactionData.push({ foo: "bar" });
-    storage.interactionData.push({ foo: "bar" });
-    storage.interactionData.clear();
-    equal(storage.interactionData.get().length, 0,
-          "after clearing, interaction data is zero length");
-  });
-
-  test("get interaction data returns all data", function() {
-    storage.interactionData.push({ foo: "old2" });
-    storage.interactionData.clear();
-    storage.interactionData.push({ foo: "old1" });
-    var d = storage.interactionData.get();
-    equal(d.length, 1, "get() returns complete unpublished data blobs");
-    equal(d[0].foo, 'old1', "get() returns complete unpublished data blobs");
-  });
 }());
 
diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js
index 8b505f997d4680903d922e8de6e2a90d78f08cc1..fc30baca72a584bef9a3ebc2470550ebdc073edf 100644
--- a/resources/static/test/mocks/xhr.js
+++ b/resources/static/test/mocks/xhr.js
@@ -21,19 +21,25 @@ BrowserID.Mocks.xhr = (function() {
   var random_cert = "eyJhbGciOiJSUzEyOCJ9.eyJpc3MiOiJpc3N1ZXIuY29tIiwiZXhwIjoxMzE2Njk1MzY3NzA3LCJwdWJsaWMta2V5Ijp7ImFsZ29yaXRobSI6IlJTIiwibiI6IjU2MDYzMDI4MDcwNDMyOTgyMzIyMDg3NDE4MTc2ODc2NzQ4MDcyMDM1NDgyODk4MzM0ODExMzY4NDA4NTI1NTk2MTk4MjUyNTE5MjY3MTA4MTMyNjA0MTk4MDA0NzkyODQ5MDc3ODY4OTUxOTA2MTcwODEyNTQwNzEzOTgyOTU0NjUzODEwNTM5OTQ5Mzg0NzEyNzczMzkwMjAwNzkxOTQ5NTY1OTAzNDM5NTIxNDI0OTA5NTc2ODMyNDE4ODkwODE5MjA0MzU0NzI5MjE3MjA3MzYwMTA1OTA2MDM5MDIzMjk5NTYxMzc0MDk4OTQyNzg5OTk2NzgwMTAyMDczMDcxNzYwODUyODQxMDY4OTg5ODYwNDAzNDMxNzM3NDgwMTgyNzI1ODUzODk5NzMzNzA2MDY5IiwiZSI6IjY1NTM3In0sInByaW5jaXBhbCI6eyJlbWFpbCI6InRlc3R1c2VyQHRlc3R1c2VyLmNvbSJ9fQ.aVIO470S_DkcaddQgFUXciGwq2F_MTdYOJtVnEYShni7I6mqBwK3fkdWShPEgLFWUSlVUtcy61FkDnq2G-6ikSx1fUZY7iBeSCOKYlh6Kj9v43JX-uhctRSB2pI17g09EUtvmb845EHUJuoowdBLmLa4DSTdZE-h4xUQ9MsY7Ik";
 
   /**
-   * This is the results table, the keys are the request type, url, and
+   * This is the responses table, the keys are the request type, url, and
    * a "selector" for testing.  The right is the expected return value, already
    * decoded.  If a result is "undefined", the request's error handler will be
    * called.
    */
   var xhr = {
-    results: {
+    // Keep track of the last request made to each wsapi call.  keyed only on
+    // url - for instince - instead of "get /wsapi/session_context
+    // valid", the key would only be "/wsapi/session_context"
+    requests: {},
+
+    responses: {
       "get /wsapi/session_context valid": contextInfo,
       // We are going to test for XHR failures for session_context using
       // the flag contextAjaxError.
       "get /wsapi/session_context contextAjaxError": undefined,
       "get /wsapi/email_for_token?token=token valid": { email: "testuser@testuser.com" },
       "get /wsapi/email_for_token?token=token mustAuth": { email: "testuser@testuser.com", must_auth: true },
+      "get /wsapi/email_for_token?token=token needsPassword": { email: "testuser@testuser.com", needs_password: true },
       "get /wsapi/email_for_token?token=token invalid": { success: false },
       "post /wsapi/authenticate_user valid": { success: true, userid: 1 },
       "post /wsapi/authenticate_user invalid": { success: false },
@@ -85,7 +91,6 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/stage_email invalid": { success: false },
       "post /wsapi/stage_email throttle": 429,
       "post /wsapi/stage_email ajaxError": undefined,
-      "post /wsapi/cert_key ajaxError": undefined,
       "get /wsapi/email_addition_status?email=testuser%40testuser.com complete": { status: "complete" },
       "get /wsapi/email_addition_status?email=registered%40testuser.com pending": { status: "pending" },
       "get /wsapi/email_addition_status?email=registered%40testuser.com complete": { status: "complete" },
@@ -124,6 +129,7 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/prolong_session unauthenticated": 400,
       "post /wsapi/prolong_session ajaxError": undefined,
       "post /wsapi/interaction_data valid": { success: true },
+      "post /wsapi/interaction_data throttle": 413,
       "post /wsapi/interaction_data ajaxError": undefined
     },
 
@@ -136,61 +142,63 @@ BrowserID.Mocks.xhr = (function() {
     },
 
     useResult: function(result) {
-      xhr.resultType = result;
+      xhr.responseName = result;
     },
 
-    getLastRequest: function() {
-      return this.req;
+    getLastRequest: function(key) {
+      var req = this.request;
+      if (key) {
+        req = this.requests[key];
+      }
+
+      return req;
     },
 
-    ajax: function(obj) {
+    ajax: function(request) {
       //console.log("ajax request");
-      var type = obj.type ? obj.type.toLowerCase() : "get";
-
-      var req = this.req = {
-        type: type,
-        url: obj.url,
-        data: obj.data
-      };
+      var type = request.type ? request.type.toLowerCase() : "get";
 
+      this.request = request = _.extend(request, {
+        type: type
+      });
 
-      if(type === "post" && obj.data.indexOf("csrf") === -1) {
+      if (type === "post" && request.data.indexOf("csrf") === -1) {
         ok(false, "missing csrf token on POST request");
       }
 
-
-      var resultType = xhr.resultType;
+      var responseName = xhr.responseName;
 
       // Unless the contextAjaxError is specified, use the "valid" context info.
       // This makes it so we do not have to keep adding new items for
       // context_info for every possible result type.
-      if(req.url === "/wsapi/session_context" && resultType !== "contextAjaxError") {
-        resultType = "valid";
+      if (request.url === "/wsapi/session_context" && responseName !== "contextAjaxError") {
+        responseName = "valid";
       }
 
-      var resName = req.type + " " + req.url + " " + resultType;
+      var responseKey = request.type + " " + request.url + " " + responseName,
+          response = xhr.responses[responseKey],
+          typeofResponse = typeof response;
 
-      var result = xhr.results[resName];
+      this.requests[request.url] = request;
 
-      var type = typeof result;
-      if(type === "function") {
-        result(obj.success);
+      if (typeofResponse === "function") {
+        response(request.success);
       }
-      else if(!(type == "number" || type == "undefined")) {
-        if(obj.success) {
-          if(delay) {
+      else if (!(typeofResponse == "number" || typeofResponse == "undefined")) {
+        if (request.success) {
+          if (delay) {
             // simulate response delay
-            _.delay(obj.success, delay, result);
+            _.delay(request.success, delay, response);
           }
           else {
-            obj.success(result);
+            request.success(response);
           }
         }
       }
-      else if (obj.error) {
-        // Invalid result - either invalid URL, invalid GET/POST or
-        // invalid resultType
-        obj.error({ status: result || 400, responseText: "response text" }, "errorStatus", "errorThrown");
+      else if (request.error) {
+        // Invalid response - either invalid URL, invalid GET/POST or
+        // invalid responseName
+        request.error({ status: response || 400, responseText: "response text" }, "errorStatus", "errorThrown");
       }
     }
   };
diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js
index 287ba8393c039d0519f9c892fcc79495f62d051f..5f72fae9ff39fc56b9b95de99c2b142962d89816 100644
--- a/resources/static/test/testHelpers/helpers.js
+++ b/resources/static/test/testHelpers/helpers.js
@@ -181,7 +181,7 @@ BrowserID.TestHelpers = (function() {
         start();
       });
 
-      if(transport.resultType === "valid") {
+      if(transport.responseName === "valid") {
         transport.useResult("ajaxError");
       }
 
@@ -200,6 +200,11 @@ BrowserID.TestHelpers = (function() {
     },
 
     testKeysInObject: function(objToTest, expected, msg) {
+      if (!objToTest) {
+        ok(false, "Missing object to test against");
+        return;
+      }
+
       for(var i=0, key; key=expected[i]; ++i) {
         ok(key in objToTest, msg || ("object contains " + key));
       }
diff --git a/resources/views/add_email_address.ejs b/resources/views/add_email_address.ejs
index fa6fbd891b20f14c23532b9497da17adf3127e16..45b711cec7d8d7b53a9da83b7f2c1f933de87ca4 100644
--- a/resources/views/add_email_address.ejs
+++ b/resources/views/add_email_address.ejs
@@ -31,6 +31,20 @@
                       <%= gettext('Password must be between 8 and 80 characters long.') %>
                     </div>
                 </li>
+
+                <li class="password_entry" id="verify_password">
+                    <label class="serif" for="vpassword"><%= gettext('Verify Password') %></label>
+                    <input class="sans" id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength="80">
+
+                    <div id="vpassword_required" class="tooltip" for="vpassword">
+                      <%= gettext('Verification password is required.') %>
+                    </div>
+
+                    <div class="tooltip" id="passwords_no_match" for="vpassword">
+                      <%= gettext ('Passwords do not match.') %>
+                    </div>
+
+                </li>
             </ul>
 
             <div class="submit cf password_entry">
diff --git a/resources/views/test.ejs b/resources/views/test.ejs
index 30c4faf532c2d3c034eb70b9fe8a96402c0a3e40..5b1890c5630a9ea5cb134fe3c722dbf4c1e44fff 100644
--- a/resources/views/test.ejs
+++ b/resources/views/test.ejs
@@ -104,6 +104,9 @@
     <script src="/shared/history.js"></script>
     <script src="/shared/state_machine.js"></script>
 
+    <script src="/shared/models/models.js"></script>
+    <script src="/shared/models/interaction_data.js"></script>
+
     <script src="/shared/modules/page_module.js"></script>
     <script src="/shared/modules/xhr_delay.js"></script>
     <script src="/shared/modules/xhr_disable_form.js"></script>
@@ -155,6 +158,8 @@
     <script src="cases/shared/history.js"></script>
     <script src="cases/shared/state_machine.js"></script>
 
+    <script src="cases/shared/models/interaction_data.js"></script>
+
     <script src="cases/shared/modules/page_module.js"></script>
     <script src="cases/shared/modules/xhr_delay.js"></script>
     <script src="cases/shared/modules/xhr_disable_form.js"></script>
diff --git a/resources/views/verify_email_address.ejs b/resources/views/verify_email_address.ejs
index a73c80ead95230830d13bc8f218e5e84e878c314..1f275d3fe8c3362f98b6360c58484d401e2a9e9c 100644
--- a/resources/views/verify_email_address.ejs
+++ b/resources/views/verify_email_address.ejs
@@ -18,6 +18,7 @@
                     <label class="serif" for="email"><%= gettext('Email Address') %></label>
                     <input class="youraddress sans" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" />
                 </li>
+
                 <li class="password_entry">
                     <label class="serif" for="password"><%= gettext('Password') %></label>
                     <input class="sans" id="password" placeholder="<%= gettext('Your Password') %>" type="password" autofocus maxlength=80 />
@@ -30,6 +31,20 @@
                       <%= gettext('Password must be between 8 and 80 characters long.') %>
                     </div>
                 </li>
+
+                <li class="password_entry" id="verify_password">
+                    <label class="serif" for="vpassword"><%= gettext('Verify Password') %></label>
+                    <input class="sans" id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength="80">
+
+                    <div id="vpassword_required" class="tooltip" for="vpassword">
+                      <%= gettext('Verification password is required.') %>
+                    </div>
+
+                    <div class="tooltip" id="passwords_no_match" for="vpassword">
+                      <%= gettext ('Passwords do not match.') %>
+                    </div>
+
+                </li>
             </ul>
 
             <div class="submit cf password_entry">