diff --git a/lib/db.js b/lib/db.js
index 1f08f6e99c5d421bb53c4c4a35b5a116e452cf37..0dca5d2f9f7e01f37ef48b3a1a505e80f733ed88 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -79,6 +79,7 @@ exports.onReady = function(f) {
   'emailKnown',
   'emailToUID',
   'emailType',
+  'emailIsVerified',
   'emailsBelongToSameAccount',
   'haveVerificationSecret',
   'isStaged',
@@ -101,7 +102,9 @@ exports.onReady = function(f) {
 [
   'stageUser',
   'stageEmail',
-  'gotVerificationSecret',
+  'completeCreateUser',
+  'completeConfirmEmail',
+  'completePasswordReset',
   'removeEmail',
   'cancelAccount',
   'updatePassword',
diff --git a/lib/db/json.js b/lib/db/json.js
index 0bcfbf8a1fa9bcc5fdb8fab56c40705ff1b01e23..a1d8623e4265f1aab1dbf2ba919c75647b4da9e4 100644
--- a/lib/db/json.js
+++ b/lib/db/json.js
@@ -111,6 +111,15 @@ exports.emailKnown = function(email, cb) {
   process.nextTick(function() { cb(null, m.length > 0) });
 };
 
+exports.emailIsVerified = function(email, cb) {
+  sync();
+  var m = jsel.match(".emails ." + ESC(email), db.users);
+  process.nextTick(function() {
+    if (!m.length) cb("no such email");
+    else cb(null, m[0].verified);
+  });
+};
+
 exports.emailType = function(email, cb) {
   sync();
   var m = jsel.match(".emails ." + ESC(email), db.users);
@@ -175,7 +184,7 @@ function addEmailToAccount(userID, email, type, cb) {
     sync();
     var emails = jsel.match(":has(.id:expr(x="+ ESC(userID) +")) > .emails", db.users);
     if (emails && emails.length > 0) {
-      emails[0][email] = { type: type };
+      emails[0][email] = { type: type, verified: true };
       flush();
     }
     cb(null);
@@ -218,7 +227,7 @@ exports.stageEmail = function(existing_user, new_email, hash, cb) {
 
 exports.createUserWithPrimaryEmail = function(email, cb) {
   var emailVal = { };
-  emailVal[email] = { type: 'primary' };
+  emailVal[email] = { type: 'primary', verified: true };
   var uid = getNextUserID();
   db.users.push({
     id: uid,
@@ -270,7 +279,7 @@ exports.verificationSecretForEmail = function(email, cb) {
   }, 0);
 };
 
-exports.gotVerificationSecret = function(secret, cb) {
+function getAndDeleteRowForSecret(secret, cb) {
   sync();
   if (!db.staged.hasOwnProperty(secret)) return cb("unknown secret");
 
@@ -279,11 +288,48 @@ exports.gotVerificationSecret = function(secret, cb) {
   delete db.staged[secret];
   delete db.stagedEmails[o.email];
   flush();
-  if (o.type === 'add_account') {
+  
+  process.nextTick(function() { cb(null, o); });
+}
+
+// either a email re-verification, or an email addition - we treat these things
+// the same
+exports.completeConfirmEmail = function(secret, cb) {
+  getAndDeleteRowForSecret(secret, function(err, o) {
+    exports.emailKnown(o.email, function(err, known) {
+      function addIt() {
+        addEmailToAccount(o.existing_user, o.email, 'secondary', function(e) {
+          var hash = o.passwd;
+          if(e || typeof hash !== 'string') return cb(e, o.email, o.existing_user);
+
+          // a hash was specified, update the password for the user
+          exports.emailToUID(o.email, function(err, uid) {
+            if(err) return cb(err, o.email, o.existing_user);
+
+            exports.updatePassword(uid, hash, function(err) {
+              cb(err || null, o.email, o.existing_user);
+            });
+          });
+        });
+      }
+      if (known) {
+        removeEmailNoCheck(o.email, function (err) {
+          if (err) cb(err);
+          else addIt();
+        });
+      } else {
+        addIt();
+      }
+    });
+  });
+}
+
+exports.completeCreateUser = function(secret, cb) {
+  getAndDeleteRowForSecret(secret, function(err, o) {
     exports.emailKnown(o.email, function(err, known) {
       function createAccount() {
         var emailVal = {};
-        emailVal[o.email] = { type: 'secondary' };
+        emailVal[o.email] = { type: 'secondary', verified: true };
         var uid = getNextUserID();
         var hash = o.passwd;
         db.users.push({
@@ -310,35 +356,41 @@ exports.gotVerificationSecret = function(secret, cb) {
         createAccount();
       }
     });
-  } else if (o.type === 'add_email') {
+  });
+};
+
+exports.completePasswordReset = function(secret, cb) {
+  getAndDeleteRowForSecret(secret, function(err, o) {
     exports.emailKnown(o.email, function(err, known) {
-      function addIt() {
-        addEmailToAccount(o.existing_user, o.email, 'secondary', function(e) {
-          var hash = o.passwd;
-          if(e || hash === null) return cb(e, o.email, o.existing_user);
+      if (err) return cb(err);
 
-          // a hash was specified, update the password for the user
-          exports.emailToUID(o.email, function(err, uid) {
-            if(err) return cb(err, o.email, o.existing_user);
+      exports.emailToUID(o.email, function(err, uid) {
+        if (err) return cb(err);
 
-            exports.updatePassword(uid, hash, function(err) {
-              cb(err || null, o.email, o.existing_user);
-            });
-          });
+        // if for some reason the email is associated with a different user now than when
+        // the action was initiated, error out.
+        if (uid !== o.existing_user) {
+          return cb("cannot update password, data inconsistency");
+        }
+
+        sync();
+        // flip the verification bit on all emails for the user other than the one just verified
+        var emails = jsel.match(":has(.id:expr(x=?)) > .emails", [ uid ], db.users)[0];        
+
+        Object.keys(emails).forEach(function(email) {
+          if (email != o.email && emails[email].type === 'secondary') {
+            emails[email].verified = false;
+          } 
         });
-      }
-      if (known) {
-        removeEmailNoCheck(o.email, function (err) {
-          if (err) cb(err);
-          else addIt();
+        flush();
+
+        // update the password!
+        exports.updatePassword(uid, o.passwd, function(err) {
+          cb(err, o.email, uid);
         });
-      } else {
-        addIt();
-      }
+      });
     });
-  } else {
-    cb("internal error");
-  }
+  });
 };
 
 exports.addPrimaryEmailToAccount = function(userID, emailToAdd, cb) {
@@ -442,7 +494,7 @@ exports.addTestUser = function(email, hash, cb) {
   sync();
   removeEmailNoCheck(email, function() {
     var emailVal = {};
-    emailVal[email] = { type: 'secondary' };
+    emailVal[email] = { type: 'secondary', verified: true };
     db.users.push({
       id: getNextUserID(),
       password: hash,
diff --git a/lib/db/mysql.js b/lib/db/mysql.js
index f9d37b753a33d1c05ca30d775060a8201156e550..4b3cc13a2adaea54e84ce205be8c4a0ef2de4677 100644
--- a/lib/db/mysql.js
+++ b/lib/db/mysql.js
@@ -15,6 +15,7 @@
  *    | string passwd |    \- |*int user      |
  *    +---------------+       |*string address|
  *                            | enum type     |
+ *                            | bool verified |
  *                            +---------------+
  *
  *
@@ -72,6 +73,7 @@ const schemas = [
     "user BIGINT NOT NULL," +
     "address VARCHAR(255) UNIQUE NOT NULL," +
     "type ENUM('secondary', 'primary') DEFAULT 'secondary' NOT NULL," +
+    "verified BOOLEAN DEFAULT TRUE NOT NULL, " +
     "FOREIGN KEY user_fkey (user) REFERENCES user(id)" +
     ") ENGINE=InnoDB;",
 
@@ -224,6 +226,17 @@ exports.emailType = function(email, cb) {
   );
 }
 
+exports.emailIsVerified = function(email, cb) {
+  client.query(
+    "SELECT verified FROM email WHERE address = ?", [ email ],
+    function(err, rows) {
+      if (rows && rows.length > 0) cb(err, !!rows[0].verified);
+      else cb('no such email');
+    }
+  );
+};
+
+
 exports.isStaged = function(email, cb) {
   client.query(
     "SELECT COUNT(*) as N FROM staged WHERE email = ?", [ email ],
@@ -331,8 +344,7 @@ function addEmailToUser(userID, email, type, cb) {
     });
 }
 
-
-exports.gotVerificationSecret = function(secret, cb) {
+function getAndDeleteRowForSecret(secret, cb) {
   client.query(
     "SELECT * FROM staged WHERE secret = ?", [ secret ],
     function(err, rows) {
@@ -342,45 +354,91 @@ exports.gotVerificationSecret = function(secret, cb) {
       } else if (rows.length === 0) {
         cb("unknown secret");
       } else {
-        var o = rows[0];
-
         // delete the record
         client.query("DELETE LOW_PRIORITY FROM staged WHERE secret = ?", [ secret ]);
+        cb(null, rows[0]);
+      }
+    });
+}
 
-        if (o.new_acct) {
-          var hash = o.passwd;
-          // we're creating a new account, add appropriate entries into user and email tables.
-          client.query(
-            "INSERT INTO user(passwd) VALUES(?)",
-            [ hash ],
-            function(err, info) {
-              if (err) return cb(err);
-              addEmailToUser(info.insertId, o.email, 'secondary', cb);
-            });
-        } else {
-          // ensure the expected existing_user field is populated, which it must always be when
-          // new_acct is false
-          if (typeof o.existing_user !== 'number') {
-            return cb("data inconsistency, no numeric existing user associated with staged email address");
-          }
+exports.completeCreateUser = function(secret, cb) {
+  getAndDeleteRowForSecret(secret, function(err, o) {
+    if (err) return cb(err);
+    
+    if (!o.new_acct) return cb("this verification link is not for a new account");
 
-          // we're adding an email address to an existing user account.  add appropriate entries into
-          // email table
-          var hash = o.passwd;
-          var uid = o.existing_user;
-          if (hash) {
-            exports.updatePassword(uid, hash, function(err) {
-              if (err) return cb('could not set user\'s password');
-              addEmailToUser(uid, o.email, 'secondary', cb);
-            });
-          } else {
-            addEmailToUser(uid, o.email, 'secondary', cb);
-          }
-        }
-      };
+    // we're creating a new account, add appropriate entries into user and email tables.
+    client.query(
+      "INSERT INTO user(passwd) VALUES(?)",
+      [ o.passwd ],
+      function(err, info) {
+        if (err) return cb(err);
+        addEmailToUser(info.insertId, o.email, 'secondary', cb);
+      });
+  });
+};
+
+// either a email re-verification, or an email addition - we treat these things
+// the same
+exports.completeConfirmEmail = function(secret, cb) {
+  getAndDeleteRowForSecret(secret, function(err, o) {
+    if (err) return cb(err);
+    
+    if (o.new_acct) return cb("this verification link is not for an email addition");
+
+    // ensure the expected existing_user field is populated, which it must always be when
+    // new_acct is false
+    if (typeof o.existing_user !== 'number') {
+      return cb("data inconsistency, no numeric existing user associated with staged email address");
+    }
+
+    // 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) {
+        if (err) return cb('could not set user\'s password');
+        addEmailToUser(o.existing_user, o.email, 'secondary', cb);
+      });
+    } else {
+      addEmailToUser(o.existing_user, o.email, 'secondary', cb);
+    }
+  });
+};
+
+exports.completePasswordReset = function(secret, cb) {
+  getAndDeleteRowForSecret(secret, function(err, o) {
+    if (err) return cb(err);
+    
+    if (o.new_acct || !o.passwd || !o.existing_user) {
+      return cb("this verification link is not for a password reset");
     }
-  );
-}
+
+    // verify that the email still exists in the database, and the the user with whom it is
+    // associated is the same as the user in the database
+    exports.emailToUID(o.email, function(err, uid) {
+      if (err) return cb(err);
+
+      // if for some reason the email is associated with a different user now than when
+      // the action was initiated, error out.
+      if (uid !== o.existing_user) {
+        return cb("cannot update password, data inconsistency");
+      }
+
+      // flip the verification bit on all emails for the user other than the one just verified
+      client.query(
+        'UPDATE email SET verified = FALSE WHERE user = ? AND type = ? AND address != ?',
+        [ uid, 'secondary', o.email ],
+        function(err) {
+          if (err) return cb(err);
+      
+          // update the password!
+          exports.updatePassword(uid, o.passwd, function(err) {
+            cb(err, o.email, uid);
+          });
+        });
+    });
+  });
+};
 
 exports.addPrimaryEmailToAccount = function(uid, emailToAdd, cb) {
   // we're adding an email address to an existing user account.  add appropriate entries into
@@ -471,18 +529,20 @@ exports.updatePassword = function(uid, hash, cb) {
  */
 exports.listEmails = function(uid, cb) {
   client.query(
-    'SELECT address, type FROM email WHERE user = ?',
+    'SELECT address, type, verified FROM email WHERE user = ?',
       [ uid ],
       function (err, rows) {
         if (err) cb(err);
         else {
           var emails = {};
 
-          // eventually we'll have fields in here
-          for (var i = 0; i < rows.length; i++)
-            emails[rows[i].address] = {
-              type: rows[i].type
-            };
+          for (var i = 0; i < rows.length; i++) {
+            var o = { type: rows[i].type };
+            if (o.type === 'secondary') {
+              o.verified = rows[i].verified ? true : false;
+            }
+            emails[rows[i].address] = o;
+          }
 
           cb(null,emails);
         }
diff --git a/lib/email.js b/lib/email.js
index f87e03e404bee549e7431cc0372d3b43af8f4cd3..13d24ffbb45147cbf63c130d6dc1d111d9e5b897 100644
--- a/lib/email.js
+++ b/lib/email.js
@@ -1,4 +1,4 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
+/* 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/. */
 
@@ -41,14 +41,14 @@ const templates = {
     template: fs.readFileSync(path.join(TEMPLATE_PATH, 'new.ejs')),
   },
   "reset": {
-    landing: 'verify_email_address',
+    landing: 'reset_password',
     subject: _("Reset Persona password"), 
     template: fs.readFileSync(path.join(TEMPLATE_PATH, 'reset.ejs')),
   },
-  "add": {
-    landing: 'add_email_address',
+  "confirm": {
+    landing: 'confirm',
     subject: _("Confirm email address for Persona"), 
-    template: fs.readFileSync(path.join(TEMPLATE_PATH, 'add.ejs')),
+    template: fs.readFileSync(path.join(TEMPLATE_PATH, 'confirm.ejs')),
   }
 };
 
@@ -118,8 +118,8 @@ exports.sendNewUserEmail = function(email, site, secret, langContext) {
   doSend('new', email, site, secret, langContext);
 };
 
-exports.sendAddAddressEmail = function(email, site, secret, langContext) {
-  doSend('add', email, site, secret, langContext);
+exports.sendConfirmationEmail = function(email, site, secret, langContext) {
+  doSend('confirm', email, site, secret, langContext);
 };
 
 exports.sendForgotPasswordEmail = function(email, site, secret, langContext) {
diff --git a/lib/load_gen/activities/add_email.js b/lib/load_gen/activities/add_email.js
index a79fc564da48605d8fdc50e9efbb986e52f6d8f7..9e240d8f6ef86541e6d3689ec8dd945940917274 100644
--- a/lib/load_gen/activities/add_email.js
+++ b/lib/load_gen/activities/add_email.js
@@ -19,7 +19,7 @@ exports.startFunc = function(cfg, cb) {
   // 5. email_addition_status is invoked some number of times while the dialog polls
   // 6. landing page is loaded:
   //   6a. session_context
-  //   6b. complete_email_addition
+  //   6b. complete_email_confirmation
   // 7. email_addition_status returns 'complete'
   // 8. a key is generated and added
 
@@ -76,7 +76,7 @@ exports.startFunc = function(cfg, cb) {
         var token = r.body;
 
         // and simulate clickthrough
-        wcli.post(cfg, '/wsapi/complete_email_addition', context, {
+        wcli.post(cfg, '/wsapi/complete_email_confirmation', context, {
           token: token
         }, function (err, r) {
           try {
diff --git a/lib/static/email_templates/add.ejs b/lib/static/email_templates/confirm.ejs
similarity index 66%
rename from lib/static/email_templates/add.ejs
rename to lib/static/email_templates/confirm.ejs
index fb928499b4f11710331a8ae83d7d8b4de035e59a..ad8758855cdd0d04cc17432dd8e30e0fc16a3b0d 100644
--- a/lib/static/email_templates/add.ejs
+++ b/lib/static/email_templates/confirm.ejs
@@ -3,7 +3,7 @@
 <%= format(gettext('Click to confirm this email address and automatically sign in to %s'), [site]) %>
 <%= link %>
 
-<%= format(gettext('Note: If you are NOT trying to sign into this website, please ignore this email.'), [site]) %>
+<%= gettext('Note: If you are NOT trying to sign into this website, please ignore this email.') %>
 
 <%= gettext('Thank you,') %>
 <%= gettext('The Persona team') %>
diff --git a/lib/static/views.js b/lib/static/views.js
index 65a5d64eb9b8d3f599f76563fc5103fd5ba96b38..957bfb09c92d0b3d1e6ec25d0392c9222d6f2530 100644
--- a/lib/static/views.js
+++ b/lib/static/views.js
@@ -174,10 +174,23 @@ exports.setup = function(app) {
     });
   });
 
+  // This page can be removed a couple weeks after this code ships into production,
+  // we're leaving it here to not break outstanding emails
   app.get("/add_email_address", function(req,res) {
-    renderCachableView(req, res, 'add_email_address.ejs', {title: 'Verify Email Address', fullpage: false});
+    renderCachableView(req, res, 'confirm.ejs', {title: 'Verify Email Address', fullpage: false});
   });
 
+
+  app.get("/reset_password", function(req,res) {
+    renderCachableView(req, res, 'confirm.ejs', {title: 'Reset Password'});
+  });
+
+  app.get("/confirm", function(req,res) {
+    renderCachableView(req, res, 'confirm.ejs', {title: 'Confirm Email'});
+  });
+
+
+
   // serve up testing templates.  but NOT in staging or production.  see GH-1044
   if ([ 'https://login.persona.org', 'https://login.anosrep.org' ].indexOf(config.get('public_url')) === -1) {
     // serve test.ejs to /test or /test/ or /test/index.html
diff --git a/lib/wsapi/cert_key.js b/lib/wsapi/cert_key.js
index 0f0c81ef3f8d7336db02f0ad90a6719fcc086957..1f373b92a0a5377351cf2d56619d8eef1ed4e694 100644
--- a/lib/wsapi/cert_key.js
+++ b/lib/wsapi/cert_key.js
@@ -24,15 +24,22 @@ exports.process = function(req, res) {
     // not same account? big fat error
     if (!owned) return httputils.badRequest(res, "that email does not belong to you");
 
-    // forward to the keysigner!
-    var keysigner = urlparse(config.get('keysigner_url'));
-    keysigner.path = '/wsapi/cert_key';
-    forward(keysigner, req, res, function(err) {
-      if (err) {
-        logger.error("error forwarding request to keysigner: " + err);
-        httputils.serverError(res, "can't contact keysigner");
-        return;
-      }
+    // secondary addresses in the database may be "unverified".  this occurs when
+    // a user forgets their password.  We will not issue certs for unverified email
+    // addresses
+    db.emailIsVerified(req.body.email, function(err, verified) {
+      if (!verified) return httputils.forbidden(res, "that email requires (re)verification");
+
+      // forward to the keysigner!
+      var keysigner = urlparse(config.get('keysigner_url'));
+      keysigner.path = '/wsapi/cert_key';
+      forward(keysigner, req, res, function(err) {
+        if (err) {
+          logger.error("error forwarding request to keysigner: " + err);
+          httputils.serverError(res, "can't contact keysigner");
+          return;
+        }
+      });
     });
   });
 };
diff --git a/lib/wsapi/complete_email_addition.js b/lib/wsapi/complete_email_confirmation.js
similarity index 85%
rename from lib/wsapi/complete_email_addition.js
rename to lib/wsapi/complete_email_confirmation.js
index 53ddb3625200c603ec1396aceb3b79a415143a07..fefdd1fc31d6fd9da4f76aadf908f6cfa6441165 100644
--- a/lib/wsapi/complete_email_addition.js
+++ b/lib/wsapi/complete_email_confirmation.js
@@ -2,6 +2,12 @@
  * 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/. */
 
+/* This api is hit in two cases:
+ *  + the final step in adding a new email to your account
+ *  + the final step in re-verifying an email in your account after
+ *    password reset
+ */
+
 const
 db = require('../db.js'),
 logger = require('../logging.js').logger,
@@ -18,11 +24,10 @@ exports.args = ['token'];
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  // in order to complete an email addition, one of the following must be true:
+  // in order to complete an email confirmation, one of the following must be true:
   //
   // 1. you must already be authenticated as the user who initiated the verification
   // 2. you must provide the password of the initiator.
-
   db.authForVerificationSecret(req.body.token, function(err, initiator_hash, initiator_uid) {
     if (err) {
       logger.info("unknown verification secret: " + err);
@@ -47,7 +52,7 @@ exports.process = function(req, res) {
     }
 
     function postAuthentication() {
-      db.gotVerificationSecret(req.body.token, function(e, email, uid) {
+      db.completeConfirmEmail(req.body.token, function(e, email, uid) {
         if (e) {
           logger.warn("couldn't complete email verification: " + e);
           wsapi.databaseDown(res, e);
diff --git a/lib/wsapi/complete_reset.js b/lib/wsapi/complete_reset.js
new file mode 100644
index 0000000000000000000000000000000000000000..b48f6582e94814fa91c730f4e68c760a6c1b4ae2
--- /dev/null
+++ b/lib/wsapi/complete_reset.js
@@ -0,0 +1,88 @@
+/* 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/. */
+
+const
+db = require('../db.js'),
+logger = require('../logging.js').logger,
+wsapi = require('../wsapi.js'),
+bcrypt = require('../bcrypt.js'),
+httputils = require('../httputils.js'),
+config = require('../configuration.js');
+
+exports.method = 'post';
+exports.writes_db = true;
+exports.authed = false;
+// NOTE: this API also takes a 'pass' parameter which is required
+// when a user has a null password (only primaries on their acct)
+exports.args = ['token'];
+exports.i18n = true;
+
+exports.process = function(req, res) {
+  // in order to complete a password reset, one of the following must be true:
+  //
+  // 1. you are using the same browser to complete the email verification as you
+  //    used to start it
+  // 2. you have provided the password chosen by the initiator of the verification
+  //    request
+
+  // is this the same browser?
+  if (typeof req.session.pendingReset === 'string' &&
+      req.body.token === req.session.pendingReset) {
+    return postAuthentication();
+  }
+  // is a password provided?
+  else if (typeof req.body.pass === 'string') {
+    return db.authForVerificationSecret(req.body.token, function(err, hash) {
+      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);
+          return httputils.serviceUnavailable(res, "server is too busy");
+        } else if (!success) {
+          return httputils.authRequired(res, "password mismatch");
+        } else {
+          return postAuthentication();
+        }
+      });
+    });
+  } else {
+    return httputils.authRequired(res, 'Provide your password');
+  }
+
+  function postAuthentication() {
+    db.haveVerificationSecret(req.body.token, function(err, known) {
+      if (err) return wsapi.databaseDown(res, err);
+
+      if (!known) {
+        // clear the pendingReset token from the session if we find no such
+        // token in the database
+        delete req.session.pendingReset;
+        return res.json({ success: false} );
+      }
+
+      db.completePasswordReset(req.body.token, function(err, email, uid) {
+        if (err) {
+          logger.warn("couldn't complete email verification: " + err);
+          wsapi.databaseDown(res, err);
+        } else {
+          // clear the pendingReset token from the session once we
+          // successfully complete password reset
+          delete req.session.pendingReset;
+
+          // 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 });
+        }
+      });
+    });
+  }
+};
diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js
index 7a65ec488f52322b0010ca8bfd33210324749bcf..5e253d38a5f7ca5b766796c6dcdab5936e07adf3 100644
--- a/lib/wsapi/complete_user_creation.js
+++ b/lib/wsapi/complete_user_creation.js
@@ -68,7 +68,7 @@ exports.process = function(req, res) {
         return res.json({ success: false} );
       }
 
-      db.gotVerificationSecret(req.body.token, function(err, email, uid) {
+      db.completeCreateUser(req.body.token, function(err, email, uid) {
         if (err) {
           logger.warn("couldn't complete email verification: " + err);
           wsapi.databaseDown(res, err);
diff --git a/lib/wsapi/email_for_token.js b/lib/wsapi/email_for_token.js
index ac0a5d9543514ac4cb74c1ab3986eded0ad63421..41a3c40272fc8c9139433c775373ce1097927d03 100644
--- a/lib/wsapi/email_for_token.js
+++ b/lib/wsapi/email_for_token.js
@@ -4,12 +4,18 @@
 
 const
 db = require('../db.js'),
-httputils = require('../httputils.js');
+httputils = require('../httputils.js'),
+logger = require('../logging.js').logger;
 
-/* First half of account creation.  Stages a user account for creation.
- * this involves creating a secret url that must be delivered to the
- * user via their claimed email address.  Upon timeout expiry OR clickthrough
- * the staged user account transitions to a valid user account
+/* Given a verification secret (a "token", delivered via email), return the
+ * email address associated with this token.
+ *
+ * This call also returns a hint to the UI, regarding whether completing the
+ * email verification that this token will require the user to enter their
+ * password.
+ *
+ * These two things are conflated into a single call as a performance
+ * optimization.
  */
 
 exports.method = 'get';
@@ -19,75 +25,66 @@ exports.args = ['token'];
 exports.i18n = false;
 
 exports.process = function(req, res) {
+
   db.emailForVerificationSecret(req.query.token, function(err, email, uid, hash) {
     if (err) {
       if (err === 'database unavailable') {
-        httputils.serviceUnavailable(res, err);
+        return httputils.serviceUnavailable(res, err);
       } else {
-        res.json({
+        return res.json({
           success: false,
           reason: err
         });
       }
-    } else {
-      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;
-        }
-        else if (!uid && typeof req.session.pendingCreation === 'string' &&
-                 req.query.token === req.session.pendingCreation) {
-          must_auth = false;
-        }
+    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;
 
-        res.json({
-          success: true,
-          email: email,
-          must_auth: must_auth
-        });
+      if (uid && req.session.userid === uid) {
+        must_auth = false;
       }
-
-      // 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;
       }
+      else if (typeof req.session.pendingReset === 'string' &&
+               req.query.token === req.session.pendingReset)
+      {
+        must_auth = false;
+      }
+      // NOTE: for reverification, we require you're authenticated.  it's not enough
+      // to be on the same browser - that path is nonsensical because you must be
+      // authenticated to initiate a re-verification.
 
-      if (!hash) {
-        if (!uid) {
-          needsPassword();
-        } else {
-          db.checkAuth(uid, function(err, hash) {
-            if (err) {
-              return res.json({
-                success: false,
-                reason: err
-              });
-            }
+      res.json({
+        success: true,
+        email: email,
+        must_auth: must_auth
+      });
+    }
 
-            if (!hash) {
-              needsPassword();
-            } else {
-              checkMustAuth();
-            }
+    if (!hash) {
+      // if no password is set in the stage table, this is probably an email addition
+      db.checkAuth(uid, function(err, hash) {
+        if (err) {
+          return res.json({
+            success: false,
+            reason: err
+          });
+        } else if (!hash) {
+          return res.json({
+            success: false,
+            reason: "missing password for user"
           });
         }
-      } else {
         checkMustAuth();
-      }
-
+      });
+    } else {
+      checkMustAuth();
     }
   });
 };
diff --git a/lib/wsapi/email_reverify_status.js b/lib/wsapi/email_reverify_status.js
new file mode 100644
index 0000000000000000000000000000000000000000..5068459c3312e7f0fc3baf2e48b5a6a9319f2b1c
--- /dev/null
+++ b/lib/wsapi/email_reverify_status.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+const
+db = require('../db.js'),
+wsapi = require('../wsapi.js');
+
+/* A polled API which returns whether the user has completed reverification
+ * of an email address
+ */
+
+exports.method = 'get';
+exports.writes_db = false;
+exports.authed = 'assertion';
+exports.args = ['email'];
+exports.i18n = false;
+
+exports.process = function(req, res) {
+  var email = req.query.email;
+  
+  // For simplicity, all we check is if an email is verified.  We do not check that
+  // the email is owned by the currently authenticated user, nor that the verification
+  // secret still exists.  These checks would require more database interactions, and
+  // other calls will fail in such circumstances.
+
+  // is the address verified?
+  db.emailIsVerified(email, function(err, verified) {
+    if (err) return wsapi.databaseDown(res, err);
+
+    res.json({ status: verified ? 'complete' : 'pending' });
+  });
+};
diff --git a/lib/wsapi/password_reset_status.js b/lib/wsapi/password_reset_status.js
new file mode 100644
index 0000000000000000000000000000000000000000..6059eac063238f9507b52c893ea01fc663c19428
--- /dev/null
+++ b/lib/wsapi/password_reset_status.js
@@ -0,0 +1,50 @@
+/* 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/. */
+
+const
+db = require('../db.js'),
+wsapi = require('../wsapi.js'),
+logger = require('../logging.js').logger,
+httputils = require('../httputils.js'),
+sanitize = require('../sanitize.js');
+
+exports.method = 'get';
+exports.writes_db = false;
+exports.authed = false;
+exports.args = ['email'];
+exports.i18n = false;
+
+exports.process = function(req, res) {
+  var email = req.query.email;
+
+  try {
+    sanitize(email).isEmail();
+  } catch(e) {
+    var msg = "invalid arguments: " + e;
+    logger.warn("bad request received: " + msg);
+    return httputils.badRequest(res, msg);
+  }
+
+  // if the email is in the staged table, we are not complete yet.
+  // if the email is not in the staged table -
+  //   * if we are authenticated as the owner of the email we're done
+  //   * if we are not authenticated as the owner of the email, we must auth
+  db.isStaged(email, function(err, staged) {
+    if (err) wsapi.databaseDown(res, err);
+    
+    if (staged) {
+      return res.json({ status: 'pending' });
+    } else {
+      if (wsapi.isAuthed(req, 'assertion')) {
+        db.userOwnsEmail(req.session.userid, email, function(err, owned) {
+          if (err) wsapi.databaseDown(res, err);
+          else if (owned) res.json({ status: 'complete', userid: req.session.userid });
+          else res.json({ status: 'mustAuth' });
+        });
+      } else {
+        return res.json({ status: 'mustAuth' });
+      }
+    }
+  });
+};
diff --git a/lib/wsapi/stage_email.js b/lib/wsapi/stage_email.js
index 9ad39e994ae716a7dc4d2bf312c6e902c7eaa38d..1e9d8cdb4f7cdad18008714885ff85100b1fc512 100644
--- a/lib/wsapi/stage_email.js
+++ b/lib/wsapi/stage_email.js
@@ -11,11 +11,7 @@ email = require('../email.js'),
 sanitize = require('../sanitize'),
 config = require('../configuration');
 
-/* First half of account creation.  Stages a user account for creation.
- * this involves creating a secret url that must be delivered to the
- * user via their claimed email address.  Upon timeout expiry OR clickthrough
- * the staged user account transitions to a valid user account
- */
+/* Stage an email for addition to a user's account.  Causes email to be sent. */
 
 exports.method = 'post';
 exports.writes_db = true;
@@ -92,7 +88,7 @@ exports.process = function(req, res) {
 
             res.json({ success: true });
             // let's now kick out a verification email!
-            email.sendAddAddressEmail(req.body.email, req.body.site, secret, langContext);
+            email.sendConfirmationEmail(req.body.email, req.body.site, secret, langContext);
           });
         } catch(e) {
           // we should differentiate tween' 400 and 500 here.
diff --git a/lib/wsapi/stage_reset.js b/lib/wsapi/stage_reset.js
new file mode 100644
index 0000000000000000000000000000000000000000..a8aefbbcbdba61397c637ad9f41761ac0bb340ec
--- /dev/null
+++ b/lib/wsapi/stage_reset.js
@@ -0,0 +1,105 @@
+/* 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/. */
+
+const
+db = require('../db.js'),
+wsapi = require('../wsapi.js'),
+httputils = require('../httputils'),
+logger = require('../logging.js').logger,
+email = require('../email.js'),
+sanitize = require('../sanitize'),
+config = require('../configuration');
+
+/* First half of account creation.  Stages a user account for creation.
+ * this involves creating a secret url that must be delivered to the
+ * user via their claimed email address.  Upon timeout expiry OR clickthrough
+ * the staged user account transitions to a valid user account
+ */
+
+exports.method = 'post';
+exports.writes_db = true;
+exports.authed = false;
+exports.args = ['email','site','pass'];
+exports.i18n = true;
+
+exports.process = function(req, res) {
+  // a password *must* be supplied to this call iff the user's password
+  // is currently NULL - this would occur in the case where this is the
+  // first secondary address to be added to an account
+
+  // validate
+  try {
+    sanitize(req.body.email).isEmail();
+    sanitize(req.body.site).isOrigin();
+  } catch(e) {
+    var msg = "invalid arguments: " + e;
+    logger.warn("bad request received: " + msg);
+    return httputils.badRequest(res, msg);
+  }
+
+  var err = wsapi.checkPassword(req.body.pass);
+  if (err) {
+    logger.warn("invalid password received: " + err);
+    return httputils.badRequest(res, err);
+  }
+
+  db.lastStaged(req.body.email, function (err, last) {
+    if (err) return wsapi.databaseDown(res, err);
+
+    if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) {
+      logger.warn('throttling request to stage email address ' + req.body.email + ', only ' +
+                  ((new Date() - last) / 1000.0) + "s elapsed");
+      return httputils.throttled(res, "Too many emails sent to that address, try again later.");
+    }
+
+    db.emailToUID(req.body.email, function(err, uid) {
+      if (err) {
+        logger.info("reset password fails: " + err);
+        return res.json({ success: false });
+      }
+
+      if (!uid) {
+        return res.json({
+          reason: "No such email address.",
+          success: false
+        });
+      }
+
+      // staging a user logs you out.
+      wsapi.clearAuthenticatedUser(req.session);
+
+      // now bcrypt the password
+      wsapi.bcryptPassword(req.body.pass, function (err, hash) {
+        if (err) {
+          if (err.indexOf('exceeded') != -1) {
+            logger.warn("max load hit, failing on auth request with 503: " + err);
+            return httputils.serviceUnavailable(res, "server is too busy");
+          }
+          logger.error("can't bcrypt: " + err);
+          return res.json({ success: false });
+        }
+
+        // on failure stageEmail may throw
+        try {
+          db.stageEmail(uid, req.body.email, hash, function(err, secret) {
+            if (err) return wsapi.databaseDown(res, err);
+
+            var langContext = wsapi.langContext(req);
+
+            // store the email being added in session data
+            req.session.pendingReset = secret;
+            
+            res.json({ success: true });
+
+            // let's now kick out a verification email!
+            email.sendForgotPasswordEmail(req.body.email, req.body.site, secret, langContext);
+          });
+        } catch(e) {
+          // we should differentiate tween' 400 and 500 here.
+          httputils.badRequest(res, e.toString());
+        }
+      });
+    });
+  });
+};
diff --git a/lib/wsapi/stage_reverify.js b/lib/wsapi/stage_reverify.js
new file mode 100644
index 0000000000000000000000000000000000000000..885fcb57ce7c05081dc3c8c06d70ecb16ce0110a
--- /dev/null
+++ b/lib/wsapi/stage_reverify.js
@@ -0,0 +1,68 @@
+/* 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/. */
+
+const
+db = require('../db.js'),
+wsapi = require('../wsapi.js'),
+httputils = require('../httputils'),
+logger = require('../logging.js').logger,
+email = require('../email.js'),
+sanitize = require('../sanitize'),
+config = require('../configuration');
+
+/* Stage an email for re-verification (i.e. after account password reset).
+ * Causes an email to be sent. */
+
+exports.method = 'post';
+exports.writes_db = true;
+exports.authed = 'assertion';
+exports.args = ['email','site'];
+exports.i18n = true;
+
+exports.process = function(req, res) {
+  // validate
+  try {
+    sanitize(req.body.email).isEmail();
+    sanitize(req.body.site).isOrigin();
+  } catch(e) {
+    var msg = "invalid arguments: " + e;
+    logger.warn("bad request received: " + msg);
+    return httputils.badRequest(res, msg);
+  }
+
+  // Note, we do no throttling of emails in this case.  Because this call requires
+  // authentication, protect a user from themselves could cause more harm than good,
+  // specifically we would be removing a user available workaround (i.e. a cosmic ray
+  // hits our email delivery, user doesn't get an email in 30s.  User tries again.)
+
+  // one may only reverify an email that is owned and unverified
+  db.userOwnsEmail(req.session.userid, req.body.email, function(err, owned) {
+    if (err) return res.json({ success: false, reason: err });
+    if (!owned) return res.json({ success: false, reason: 'you don\'t control that email address' });
+
+    db.emailIsVerified(req.body.email, function(err, verified) { 
+      if (err) return res.json({ success: false, reason: err });
+      if (verified) return res.json({ success: false, reason: 'email is already verified' });
+
+      try {
+        // on failure stageEmail may throw
+        db.stageEmail(req.session.userid, req.body.email, undefined, function(err, secret) {
+          if (err) return wsapi.databaseDown(res, err);
+
+          var langContext = wsapi.langContext(req);
+          
+          // store the email being reverified
+          req.session.pendingReverification = secret;
+          
+          res.json({ success: true });
+          // let's now kick out a verification email!
+          email.sendConfirmationEmail(req.body.email, req.body.site, secret, langContext);
+        });
+      } catch(e) {
+        // we should differentiate tween' 400 and 500 here.
+        httputils.badRequest(res, e.toString());
+      }
+    });
+  });
+};
diff --git a/resources/static/common/js/network.js b/resources/static/common/js/network.js
index 26397bc3ea7beb72971b3b55455628da763a44b4..ee7de6e0cbaeb01a0e44f7a05757a5a4bb70b0a5 100644
--- a/resources/static/common/js/network.js
+++ b/resources/static/common/js/network.js
@@ -83,6 +83,54 @@ BrowserID.Network = (function() {
     }
   }
 
+  function stageAddressForVerification(data, wsapiName, onComplete, onFailure) {
+    post({
+      url: wsapiName,
+      data: data,
+      success: function(status) {
+        complete(onComplete, status.success);
+      },
+      error: function(info) {
+        // 429 is throttling.
+        if (info.network.status === 429) {
+          complete(onComplete, false);
+        }
+        else complete(onFailure, info);
+      }
+    });
+  }
+
+  function handleAddressVerifyCheckResponse(onComplete, status, textStatus, jqXHR) {
+    if (status.status === 'complete') {
+      // The user at this point can ONLY be logged in with password
+      // authentication. Once the registration is complete, that means
+      // the server has updated the user's cookies and the user is
+      // officially authenticated.
+      auth_status = 'password';
+
+      if (status.userid) setUserID(status.userid);
+    }
+    complete(onComplete, status.status);
+  }
+
+  function completeAddressVerification(wsapiName, token, password, onComplete, onFailure) {
+      post({
+        url: wsapiName,
+        data: {
+          token: token,
+          pass: password
+        },
+        success: function(status, textStatus, jqXHR) {
+          // If the user has successfully completed an address verification,
+          // they are authenticated to the password status.
+          if (status.success) auth_status = "password";
+          complete(onComplete, status.success);
+        },
+        error: onFailure
+      });
+
+    }
+
   var Network = {
     /**
      * Initialize - Clear all context info. Used for testing.
@@ -161,7 +209,7 @@ BrowserID.Network = (function() {
     },
 
     withContext: function(onComplete, onFailure) {
-      withContext(onComplete, onFailure)
+      withContext(onComplete, onFailure);
     },
 
     /**
@@ -213,24 +261,12 @@ BrowserID.Network = (function() {
      * @param {function} [onFailure] - Called on XHR failure.
      */
     createUser: function(email, password, origin, onComplete, onFailure) {
-      post({
-        url: "/wsapi/stage_user",
-        data: {
-          email: email,
-          pass: password,
-          site : origin
-        },
-        success: function(status) {
-          complete(onComplete, status.success);
-        },
-        error: function(info) {
-          // 429 is throttling.
-          if (info.network.status === 429) {
-            complete(onComplete, false);
-          }
-          else complete(onFailure, info);
-        }
-      });
+      var postData = {
+        email: email,
+        pass: password,
+        site : origin
+      };
+      stageAddressForVerification(postData, "/wsapi/stage_user", onComplete, onFailure);
     },
 
     /**
@@ -265,17 +301,7 @@ BrowserID.Network = (function() {
     checkUserRegistration: function(email, onComplete, onFailure) {
       get({
         url: "/wsapi/user_creation_status?email=" + encodeURIComponent(email),
-        success: function(status, textStatus, jqXHR) {
-          if (status.status === 'complete' && status.userid) {
-            // The user at this point can ONLY be logged in with password
-            // authentication. Once the registration is complete, that means
-            // the server has updated the user's cookies and the user is
-            // officially authenticated.
-            auth_status = 'password';
-            setUserID(status.userid);
-          }
-          complete(onComplete, status.status);
-        },
+        success: handleAddressVerifyCheckResponse.curry(onComplete),
         error: onFailure
       });
     },
@@ -288,19 +314,7 @@ BrowserID.Network = (function() {
      * @param {function} [onComplete] - Called when complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    completeUserRegistration: function(token, password, onComplete, onFailure) {
-      post({
-        url: "/wsapi/complete_user_creation",
-        data: {
-          token: token,
-          pass: password
-        },
-        success: function(status, textStatus, jqXHR) {
-          complete(onComplete, status.success);
-        },
-        error: onFailure
-      });
-    },
+    completeUserRegistration: completeAddressVerification.curry("/wsapi/complete_user_creation"),
 
     /**
      * Call with a token to prove an email address ownership.
@@ -311,19 +325,7 @@ BrowserID.Network = (function() {
      * with one boolean parameter that specifies the validity of the token.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    completeEmailRegistration: function(token, password, onComplete, onFailure) {
-      post({
-        url: "/wsapi/complete_email_addition",
-        data: {
-          token: token,
-          pass: password
-        },
-        success: function(status, textStatus, jqXHR) {
-          complete(onComplete, status.success);
-        },
-        error: onFailure
-      });
-    },
+    completeEmailRegistration: completeAddressVerification.curry("/wsapi/complete_email_confirmation"),
 
     /**
      * Request a password reset for the given email address.
@@ -335,14 +337,74 @@ BrowserID.Network = (function() {
      * @param {function} [onFailure] - Called on XHR failure.
      */
     requestPasswordReset: function(email, password, origin, onComplete, onFailure) {
-      if (email) {
-        Network.createUser(email, password, origin, onComplete, onFailure);
-      } else {
-        // TODO: if no email is provided, then what?
-        throw "no email provided to password reset";
-      }
+      var postData = {
+        email: email,
+        pass: password,
+        site : origin
+      };
+      stageAddressForVerification(postData, "/wsapi/stage_reset", onComplete, onFailure);
+    },
+
+    /**
+     * Complete email reset password
+     * @method completePasswordReset
+     * @param {string} token - token to register for.
+     * @param {string} password
+     * @param {function} [onComplete] - Called when complete.
+     * @param {function} [onFailure] - Called on XHR failure.
+     */
+    completePasswordReset: completeAddressVerification.curry("/wsapi/complete_reset"),
+
+    /**
+     * Check the registration status of a password reset
+     * @method checkPasswordReset
+     * @param {function} [onsuccess] - called when complete.
+     * @param {function} [onfailure] - called on xhr failure.
+     */
+    checkPasswordReset: function(email, onComplete, onFailure) {
+      get({
+        url: "/wsapi/password_reset_status?email=" + encodeURIComponent(email),
+        success: handleAddressVerifyCheckResponse.curry(onComplete),
+        error: onFailure
+      });
+    },
+
+    /**
+     * Stage an email reverification.
+     * @method requestEmailReverify
+     * @param {string} email
+     * @param {string} origin - site user is trying to sign in to.
+     * @param {function} [onComplete] - Callback to call when complete.
+     * @param {function} [onFailure] - Called on XHR failure.
+     */
+    requestEmailReverify: function(email, origin, onComplete, onFailure) {
+      var postData = {
+        email: email,
+        site : origin
+      };
+      stageAddressForVerification(postData, "/wsapi/stage_reverify", onComplete, onFailure);
+    },
+
+    // the verification page for reverifying an email and adding an email to an
+    // account are the same, both are handled by the /confirm page. the
+    // /confirm page uses the verifyEmail function.  completeEmailReverify is
+    // not needed.
+
+    /**
+     * Check the registration status of an email reverification
+     * @method checkEmailReverify
+     * @param {function} [onsuccess] - called when complete.
+     * @param {function} [onfailure] - called on xhr failure.
+     */
+    checkEmailReverify: function(email, onComplete, onFailure) {
+      get({
+        url: "/wsapi/email_reverify_status?email=" + encodeURIComponent(email),
+        success: handleAddressVerifyCheckResponse.curry(onComplete),
+        error: onFailure
+      });
     },
 
+
     /**
      * Set the password of the current user.
      * @method setPassword
@@ -453,27 +515,14 @@ BrowserID.Network = (function() {
      * @param {function} [onFailure] - called on xhr failure.
      */
     addSecondaryEmail: function(email, password, origin, onComplete, onFailure) {
-      post({
-        url: "/wsapi/stage_email",
-        data: {
-          email: email,
-          pass: password,
-          site: origin
-        },
-        success: function(response) {
-          complete(onComplete, response.success);
-        },
-        error: function(info) {
-          // 429 is throttling.
-          if (info.network.status === 429) {
-            complete(onComplete, false);
-          }
-          else complete(onFailure, info);
-        }
-      });
+      var postData = {
+        email: email,
+        pass: password,
+        site : origin
+      };
+      stageAddressForVerification(postData, "/wsapi/stage_email", onComplete, onFailure);
     },
 
-
     /**
      * Check the registration status of an email
      * @method checkEmailRegistration
@@ -483,9 +532,7 @@ BrowserID.Network = (function() {
     checkEmailRegistration: function(email, onComplete, onFailure) {
       get({
         url: "/wsapi/email_addition_status?email=" + encodeURIComponent(email),
-        success: function(status, textStatus, jqXHR) {
-          complete(onComplete, status.status);
-        },
+        success: handleAddressVerifyCheckResponse.curry(onComplete),
         error: onFailure
       });
     },
@@ -666,6 +713,7 @@ BrowserID.Network = (function() {
       // Make sure we get context first or else we will needlessly send
       // a cookie to the server.
       withContext(function() {
+        var enabled;
         try {
           // set a test cookie with a duration of 1 second.
           // NOTE - The Android 3.3 and 4.0 default browsers will still pass
@@ -674,7 +722,7 @@ BrowserID.Network = (function() {
           // submitted input.
           // http://stackoverflow.com/questions/8509387/android-browser-not-respecting-cookies-disabled/9264996#9264996
           document.cookie = "test=true; max-age=1";
-          var enabled = document.cookie.indexOf("test") > -1;
+          enabled = document.cookie.indexOf("test") > -1;
         } catch(e) {
           enabled = false;
         }
diff --git a/resources/static/common/js/user.js b/resources/static/common/js/user.js
index b1be1696d9d8cddb49f66ba00eaa51f88060a168..5438da17921fa3b5a5889d8a37a58614b51af0a0 100644
--- a/resources/static/common/js/user.js
+++ b/resources/static/common/js/user.js
@@ -11,7 +11,8 @@ BrowserID.User = (function() {
       bid = BrowserID,
       network = bid.Network,
       storage = bid.Storage,
-      User, pollTimeout,
+      User,
+      pollTimeout,
       provisioning = bid.Provisioning,
       addressCache = {},
       primaryAuthCache = {},
@@ -100,7 +101,53 @@ BrowserID.User = (function() {
     }
   }
 
-  function registrationPoll(checkFunc, email, onSuccess, onFailure) {
+  function handleStageAddressVerifictionResponse(onComplete, staged) {
+    var status = { success: staged };
+
+    if (!staged) status.reason = "throttle";
+    // Used on the main site when the user verifies - once
+    // verification is complete, the user is redirected back to the
+    // RP and logged in.
+    var site = User.getReturnTo();
+    if (staged && site) storage.setReturnTo(site);
+
+    complete(onComplete, status);
+  }
+
+  function completeAddressVerification(completeFunc, token, password, onComplete, onFailure) {
+    User.tokenInfo(token, function(info) {
+      var invalidInfo = { valid: false };
+      if (info) {
+        completeFunc(token, password, function (valid) {
+          var result = invalidInfo;
+
+          if (valid) {
+            result = _.extend({ valid: valid }, info);
+            var email = info.email,
+                idInfo = storage.getEmail(email);
+
+            // Now that the address is verified, its verified bit has to be
+            // updated as well or else the user will be forced to verify the
+            // address again.
+            if (idInfo) {
+              idInfo.verified = true;
+              storage.addEmail(email, idInfo);
+            }
+
+            storage.setReturnTo("");
+          }
+
+          complete(onComplete, result);
+        }, onFailure);
+      } else if (onComplete) {
+        onComplete(invalidInfo);
+      }
+    }, onFailure);
+
+  }
+
+
+  function addressVerificationPoll(checkFunc, email, onSuccess, onFailure) {
     function poll() {
       checkFunc(email, function(status) {
         // registration status checks the status of the last initiated registration,
@@ -117,7 +164,7 @@ BrowserID.User = (function() {
 
           // To avoid too many address_info requests, returns from each
           // address_info request are cached.  If the user is doing
-          // a registrationPoll, it means the user was registering the address
+          // a addressVerificationPoll, it means the user was registering the address
           // and the registration has completed.  Because the status is
           // "complete" or "known", we know that the address is known, so we
           // toggle the field to be up to date.  If the known field remains
@@ -206,21 +253,21 @@ BrowserID.User = (function() {
   /**
    * Persist an email address without a keypair
    * @method persistEmail
-   * @param {string} email - Email address to persist.
-   * @param {string} type - Is the email a 'primary' or a 'secondary' address?
-   * @param {function} [onComplete] - Called on successful completion.
-   * @param {function} [onFailure] - Called on error.
+   * @param {object} options - options to save
+   * @param {string} options.email - Email address to persist.
+   * @param {string} options.type - Is the email a 'primary' or a 'secondary' address?
+   * @param {string} options.verified - If the email is 'secondary', is it verified?
    */
-  function persistEmail(email, type, onComplete, onFailure) {
-    checkEmailType(type);
-    storage.addEmail(email, {
+  function persistEmail(options) {
+    checkEmailType(options.type);
+    storage.addEmail(options.email, {
       created: new Date(),
-      type: type
+      type: options.type,
+      verified: options.verified
     });
-
-    if (onComplete) onComplete(true);
   }
 
+
   User = {
     init: function(config) {
       if (config.provisioning) {
@@ -302,18 +349,13 @@ BrowserID.User = (function() {
      * @param {function} [onFailure] - Called on error.
      */
     createSecondaryUser: function(email, password, onComplete, onFailure) {
-      network.createUser(email, password, origin, function(created) {
-        // Used on the main site when the user verifies - once verification
-        // is complete, the user is redirected back to the RP and logged in.
-        var site = User.getReturnTo();
-        if (created && site) storage.setReturnTo(site);
-        complete(onComplete, created);
-      }, onFailure);
+      network.createUser(email, password, origin,
+        handleStageAddressVerifictionResponse.curry(onComplete), onFailure);
     },
 
     /**
      * Create a primary user.
-     * @method createUser
+     * @method createPrimaryUser
      * @param {object} info
      * @param {function} onComplete - function to call on complettion.  Called
      * with two parameters - status and info.
@@ -477,9 +519,7 @@ BrowserID.User = (function() {
      * @param {function} [onSuccess] - Called to give status updates.
      * @param {function} [onFailure] - Called on error.
      */
-    waitForUserValidation: function(email, onSuccess, onFailure) {
-      registrationPoll(network.checkUserRegistration, email, onSuccess, onFailure);
-    },
+    waitForUserValidation: addressVerificationPoll.curry(network.checkUserRegistration),
 
     /**
      * Cancel the waitForUserValidation poll
@@ -517,25 +557,7 @@ BrowserID.User = (function() {
      *   with valid=false otw.
      * @param {function} [onFailure] - Called on error.
      */
-    verifyUser: function(token, password, onComplete, onFailure) {
-      User.tokenInfo(token, function(info) {
-        var invalidInfo = { valid: false };
-        if (info) {
-          network.completeUserRegistration(token, password, function (valid) {
-            var result = invalidInfo;
-
-            if(valid) {
-              result = _.extend({ valid: valid, returnTo: storage.getReturnTo() }, info);
-              storage.setReturnTo("");
-            }
-
-            complete(onComplete, result);
-          }, onFailure);
-        } else if (onComplete) {
-          onComplete(invalidInfo);
-        }
-      }, onFailure);
-    },
+    verifyUser: completeAddressVerification.curry(network.completeUserRegistration),
 
     /**
      * Check if the user can set their password.  Only returns true for users
@@ -588,18 +610,8 @@ BrowserID.User = (function() {
     requestPasswordReset: function(email, password, onComplete, onFailure) {
       User.isEmailRegistered(email, function(registered) {
         if (registered) {
-          network.requestPasswordReset(email, password, origin, function(reset) {
-            var status = { success: reset };
-
-            if (!reset) status.reason = "throttle";
-            // Used on the main site when the user verifies - once
-            // verification is complete, the user is redirected back to the
-            // RP and logged in.
-            var site = User.getReturnTo();
-            if (reset && site) storage.setReturnTo(site);
-
-            complete(onComplete, status);
-          }, onFailure);
+          network.requestPasswordReset(email, password, origin,
+            handleStageAddressVerifictionResponse.curry(onComplete), onFailure);
         }
         else if (onComplete) {
           onComplete({ success: false, reason: "invalid_user" });
@@ -607,6 +619,77 @@ BrowserID.User = (function() {
       }, onFailure);
     },
 
+    /**
+     * Verify the password reset for a user.
+     * @method completePasswordReset
+     * @param {string} token - token to verify.
+     * @param {string} password
+     * @param {function} [onComplete] - Called on completion.
+     *   Called with an object with valid, email, and origin if valid, called
+     *   with valid=false otw.
+     * @param {function} [onFailure] - Called on error.
+     */
+    completePasswordReset: completeAddressVerification.curry(network.completePasswordReset),
+
+    /**
+     * Wait for the password reset to complete
+     * @method waitForPasswordResetComplete
+     * @param {string} email - email address to check.
+     * @param {function} [onSuccess] - Called to give status updates.
+     * @param {function} [onFailure] - Called on error.
+     */
+    waitForPasswordResetComplete: addressVerificationPoll.curry(network.checkPasswordReset),
+
+    /**
+     * Cancel the waitForPasswordResetComplete poll
+     * @method cancelWaitForPasswordResetComplete
+     */
+    cancelWaitForPasswordResetComplete: cancelRegistrationPoll,
+
+    /**
+     * Request the reverification of an unverified email address
+     * @method requestEmailReverify
+     * @param {string} email
+     * @param {function} [onComplete]
+     * @param {function} [onFailure]
+     */
+    requestEmailReverify: function(email, onComplete, onFailure) {
+      var idInfo = storage.getEmail(email);
+      if (!idInfo) {
+        // user does not own this address.
+        complete(onComplete, { success: false, reason: "invalid_email" });
+      }
+      else if (idInfo.verified) {
+        // this email is already verified, cannot be reverified.
+        complete(onComplete, { success: false, reason: "verified_email" });
+      }
+      else if (!idInfo.verified) {
+        // this address is unverified, try to reverify it.
+        network.requestEmailReverify(email, origin,
+          handleStageAddressVerifictionResponse.curry(onComplete), onFailure);
+      }
+    },
+
+    // the verification page for reverifying an email and adding an email to an
+    // account are the same, both are handled by the /confirm page. the
+    // /confirm page uses the verifyEmail function.  completeEmailReverify is
+    // not needed.
+
+    /**
+     * Wait for the email reverification to complete
+     * @method waitForEmailReverifyComplete
+     * @param {string} email - email address to check.
+     * @param {function} [onSuccess] - Called to give status updates.
+     * @param {function} [onFailure] - Called on error.
+     */
+    waitForEmailReverifyComplete: addressVerificationPoll.curry(network.checkEmailReverify),
+
+    /**
+     * Cancel the waitForEmailReverifyComplete poll
+     * @method cancelWaitForEmailReverifyComplete
+     */
+    cancelWaitForEmailReverifyComplete: cancelRegistrationPoll,
+
     /**
      * Cancel the current user's account.  Remove last traces of their
      * identity.
@@ -659,29 +742,39 @@ BrowserID.User = (function() {
 
           var emails_to_add = _.difference(server_emails, client_emails);
           var emails_to_remove = _.difference(client_emails, server_emails);
+          var emails_to_update = _.intersection(client_emails, server_emails);
 
           // remove emails
           _.each(emails_to_remove, function(email) {
             storage.removeEmail(email);
           });
 
-          // keygen for new emails
-          // asynchronous
-          function addNextEmail() {
-            if (!emails_to_add || !emails_to_add.length) {
-              onComplete();
-              return;
-           }
+          // these are new emails
+          _.each(emails_to_add, function(email) {
+            var emailInfo = emails[email];
 
-            var email = emails_to_add.shift();
+            persistEmail({
+              email: email,
+              type: emailInfo.type || "secondary",
+              verified: emailInfo.verified
+            });
+          });
 
-            // extract the email type from the server response, if it
-            // doesn't exist, assume secondary
-            var type = emails[email].type || "secondary";
-            persistEmail(email, type, addNextEmail, onFailure);
-          }
+          // update the type and verified status of stored emails
+          _.each(emails_to_update, function(email) {
+            var emailInfo = emails[email],
+                storedEmailInfo = storage.getEmail(email);
+
+            _.extend(storedEmailInfo, {
+              type: emailInfo.type,
+              verified: emailInfo.verified
+            });
+
+            storage.addEmail(email, storedEmailInfo);
+          });
+
+          complete(onComplete);
 
-          addNextEmail();
         }, onFailure);
       });
     },
@@ -891,9 +984,7 @@ BrowserID.User = (function() {
      * @param {function} [onSuccess] - Called to give status updates.
      * @param {function} [onFailure] - Called on error.
      */
-    waitForEmailValidation: function(email, onSuccess, onFailure) {
-      registrationPoll(network.checkEmailRegistration, email, onSuccess, onFailure);
-    },
+    waitForEmailValidation: addressVerificationPoll.curry(network.checkEmailRegistration),
 
     /**
      * Cancel the waitForEmailValidation poll
@@ -913,25 +1004,7 @@ BrowserID.User = (function() {
      *   with valid=false otw.
      * @param {function} [onFailure] - Called on error.
      */
-    verifyEmail: function(token, password, onComplete, onFailure) {
-      network.emailForVerificationToken(token, function (info) {
-        var invalidInfo = { valid: false };
-        if (info) {
-          network.completeEmailRegistration(token, password, function (valid) {
-            var result = invalidInfo;
-
-            if(valid) {
-              result = _.extend({ valid: valid, returnTo: storage.getReturnTo() }, info);
-              storage.setReturnTo("");
-            }
-
-            complete(onComplete, result);
-          }, onFailure);
-        } else {
-          complete(onComplete, invalidInfo);
-        }
-      }, onFailure);
-    },
+    verifyEmail: completeAddressVerification.curry(network.completeEmailRegistration),
 
     /**
      * Remove an email address.
diff --git a/resources/static/dialog/js/misc/helpers.js b/resources/static/dialog/js/misc/helpers.js
index 6a0cae05bdfa6e35aa536e44c11f813eca400f13..949eac7a2f400aad97af58ff0edca8ffdfc4adef 100644
--- a/resources/static/dialog/js/misc/helpers.js
+++ b/resources/static/dialog/js/misc/helpers.js
@@ -74,7 +74,7 @@
   function createUser(email, password, callback) {
     var self=this;
     user.createSecondaryUser(email, password, function(status) {
-      if (status) {
+      if (status.success) {
         var info = { email: email, password: password };
         self.publish("user_staged", info, info);
         complete(callback, true);
@@ -92,7 +92,20 @@
     var self=this;
     user.requestPasswordReset(email, password, function(status) {
       if (status.success) {
-        self.publish("password_reset", { email: email });
+        self.publish("reset_password_staged", { email: email });
+      }
+      else {
+        tooltip.showTooltip("#could_not_add");
+      }
+      complete(callback, status.success);
+    }, self.getErrorDialog(errors.requestPasswordReset, callback));
+  }
+
+  function reverifyEmail(email, callback) {
+    var self=this;
+    user.requestEmailReverify(email, function(status) {
+      if (status.success) {
+        self.publish("reverify_email_staged", { email: email });
       }
       else {
         tooltip.showTooltip("#could_not_add");
@@ -152,6 +165,7 @@
     addEmail: addEmail,
     addSecondaryEmail: addSecondaryEmail,
     resetPassword: resetPassword,
+    reverifyEmail: reverifyEmail,
     cancelEvent: helpers.cancelEvent,
     animateClose: animateClose,
     showRPTosPP: showRPTosPP
diff --git a/resources/static/dialog/js/misc/state.js b/resources/static/dialog/js/misc/state.js
index 65fdc9b0e691806dc8929219fa4acd4b6e9710dd..b9f1f7db346016bf81d7b1a735d7f2348112adab 100644
--- a/resources/static/dialog/js/misc/state.js
+++ b/resources/static/dialog/js/misc/state.js
@@ -1,5 +1,5 @@
 /*jshint browser:true, jquery: true, forin: true, laxbreak:true */
-/*global BrowserID: true */
+/*global BrowserID: true, URLParse: 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/. */
@@ -43,6 +43,20 @@ BrowserID.State = (function() {
         },
         cancelState = self.popState.bind(self);
 
+    function handleEmailStaged(actionName, msg, info) {
+      // The unverified email has been staged, now the user has to confirm
+      // ownership of the address.  Send them off to the "verify your address"
+      // screen.
+      var actionInfo = {
+        email: info.email,
+        siteName: self.siteName
+      };
+
+      self.stagedEmail = info.email;
+      startAction(actionName, actionInfo);
+    }
+
+
     handleState("start", function(msg, info) {
       self.hostname = info.hostname;
       self.siteName = info.siteName || info.hostname;
@@ -152,22 +166,18 @@ BrowserID.State = (function() {
       }
       else if(self.resetPasswordEmail) {
         self.resetPasswordEmail = null;
-        startAction(false, "doResetPassword", info);
+        startAction(false, "doStageResetPassword", info);
       }
     });
 
-    handleState("user_staged", function(msg, info) {
-      self.stagedEmail = info.email;
-
-      _.extend(info, {
-        required: !!requiredEmail,
-        siteName: self.siteName
-      });
+    handleState("user_staged", handleEmailStaged.curry("doConfirmUser"));
 
-      startAction("doConfirmUser", info);
+    handleState("user_confirmed", function() {
+      self.email = self.stagedEmail;
+      redirectToState("email_chosen", { email: self.stagedEmail} );
     });
 
-    handleState("user_confirmed", function() {
+    handleState("staged_address_confirmed", function() {
       self.email = self.stagedEmail;
       redirectToState("email_chosen", { email: self.stagedEmail} );
     });
@@ -274,53 +284,72 @@ BrowserID.State = (function() {
         complete(info.complete);
       }
 
-      if (idInfo) {
-        mediator.publish("kpi_data", { email_type: idInfo.type });
+      if (!idInfo) {
+        throw "invalid email";
+      }
 
-        if (idInfo.type === "primary") {
-          if (idInfo.cert) {
-            // Email is a primary and the cert is available - the user can log
-            // in without authenticating with the IdP. All invalid/expired
-            // certs are assumed to have been checked and removed by this
-            // point.
-            redirectToState("email_valid_and_ready", info);
-          }
-          else {
-            // If the email is a primary and the cert is not available,
-            // throw the user down the primary flow. The primary flow will
-            // catch cases where the primary certificate is expired
-            // and the user must re-verify with their IdP.
-            redirectToState("primary_user", info);
-          }
+      mediator.publish("kpi_data", { email_type: idInfo.type });
+
+      if (idInfo.type === "primary") {
+        if (idInfo.cert) {
+          // Email is a primary and the cert is available - the user can log
+          // in without authenticating with the IdP. All invalid/expired
+          // certs are assumed to have been checked and removed by this
+          // point.
+          redirectToState("email_valid_and_ready", info);
         }
         else {
-          user.checkAuthentication(function(authentication) {
-            if (authentication === "assertion") {
-              // user must authenticate with their password, kick them over to
-              // the required email screen to enter the password.
-              startAction("doAuthenticateWithRequiredEmail", {
-                email: email,
-                secondary_auth: true,
-
-                // This is a user is already authenticated to the assertion
-                // level who has chosen a secondary email address from the
-                // pick_email screen. They would have been shown the
-                // siteTOSPP there.
-                siteTOSPP: false
-              });
-            }
-            else {
-              redirectToState("email_valid_and_ready", info);
-            }
-            oncomplete();
-          }, oncomplete);
+          // If the email is a primary and the cert is not available,
+          // throw the user down the primary flow. The primary flow will
+          // catch cases where the primary certificate is expired
+          // and the user must re-verify with their IdP.
+          redirectToState("primary_user", info);
         }
       }
+      // Anything below this point means the address is a secondary.
+      else if (!idInfo.verified) {
+        // user selected an unverified secondary email, kick them over to the
+        // verify screen.
+        redirectToState("stage_reverify_email", info);
+      }
       else {
-        throw "invalid email";
+        // Address is verified, check the authentication, if the user is not
+        // authenticated to the assertion level, force them to enter their
+        // password.
+        user.checkAuthentication(function(authentication) {
+          if (authentication === "assertion") {
+             // user must authenticate with their password, kick them over to
+            // the required email screen to enter the password.
+            startAction("doAuthenticateWithRequiredEmail", {
+              email: email,
+              secondary_auth: true,
+
+              // This is a user is already authenticated to the assertion
+              // level who has chosen a secondary email address from the
+              // pick_email screen. They would have been shown the
+              // siteTOSPP there.
+              siteTOSPP: false
+            });
+          }
+          else {
+            redirectToState("email_valid_and_ready", info);
+          }
+          oncomplete();
+        }, oncomplete);
       }
     });
 
+    handleState("stage_reverify_email", function(msg, info) {
+      // A user has selected an email that has not been verified after
+      // a password reset.  Stage the email again to be re-verified.
+      var actionInfo = {
+        email: info.email
+      };
+      startAction("doStageReverifyEmail", actionInfo);
+    });
+
+    handleState("reverify_email_staged", handleEmailStaged.curry("doConfirmReverifyEmail"));
+
     handleState("email_valid_and_ready", function(msg, info) {
       // this state is only called after all checking is done on the email
       // address.  For secondaries, this means the email has been validated and
@@ -364,21 +393,14 @@ BrowserID.State = (function() {
     handleState("forgot_password", function(msg, info) {
       // User has forgotten their password, let them reset it.  The response
       // message from the forgot_password controller will be a set_password.
-      // the set_password handler needs to know the forgotPassword email so it
-      // knows how to handle the password being set.  When the password is
-      // finally reset, the password_reset message will be raised where we must
-      // await email confirmation.
+      // the set_password handler needs to know the resetPasswordEmail so it
+      // knows how to trigger the reset_password_staged message.  At this
+      // point, the email confirmation screen will be shown.
       self.resetPasswordEmail = info.email;
-      startAction(false, "doForgotPassword", info);
+      startAction(false, "doResetPassword", info);
     });
 
-    handleState("password_reset", function(msg, info) {
-      // password_reset says the user has confirmed that they want to
-      // reset their password.  doResetPassword will attempt to invoke
-      // the create_user wsapi.  If the wsapi call is successful,
-      // the user will be shown the "go verify your account" message.
-      redirectToState("user_staged", info);
-    });
+    handleState("reset_password_staged", handleEmailStaged.curry("doConfirmResetPassword"));
 
     handleState("assertion_generated", function(msg, info) {
       self.success = true;
@@ -404,19 +426,6 @@ BrowserID.State = (function() {
       redirectToState("email_chosen", info);
     });
 
-    handleState("reset_password", function(msg, info) {
-      info = info || {};
-      // reset_password says the user has confirmed that they want to
-      // reset their password.  doResetPassword will attempt to invoke
-      // the create_user wsapi.  If the wsapi call is successful,
-      // the user will be shown the "go verify your account" message.
-
-      // We have to save the staged email address here for when the user
-      // verifies their account and user_confirmed is called.
-      self.stagedEmail = info.email;
-      startAction(false, "doResetPassword", info);
-    });
-
     handleState("add_email", function(msg, info) {
       // add_email indicates the user wishes to add an email to the account,
       // the add_email screen must be displayed.  After the user enters the
@@ -461,14 +470,7 @@ BrowserID.State = (function() {
       });
     });
 
-    handleState("email_staged", function(msg, info) {
-      self.stagedEmail = info.email;
-      _.extend(info, {
-        required: !!requiredEmail,
-        siteName: self.siteName
-      });
-      startAction("doConfirmEmail", info);
-    });
+    handleState("email_staged", handleEmailStaged.curry("doConfirmEmail"));
 
     handleState("email_confirmed", function() {
       redirectToState("email_chosen", { email: self.stagedEmail } );
diff --git a/resources/static/dialog/js/modules/actions.js b/resources/static/dialog/js/modules/actions.js
index aec8e96a38ca1abfa54e69e683aa2bd7f2134c99..02c6b5748c5eb989656bf8de8e61429172742e91 100644
--- a/resources/static/dialog/js/modules/actions.js
+++ b/resources/static/dialog/js/modules/actions.js
@@ -32,11 +32,11 @@ BrowserID.Modules.Actions = (function() {
     return module;
   }
 
-  function startRegCheckService(options, verifier, message, password) {
+  function startRegCheckService(options, verifier, message) {
     var controller = startService("check_registration", {
       verifier: verifier,
       verificationMessage: message,
-      password: password,
+      password: options.password,
       siteName: options.siteName,
       email: options.email
     });
@@ -70,7 +70,7 @@ BrowserID.Modules.Actions = (function() {
     },
 
     doConfirmUser: function(info) {
-      startRegCheckService.call(this, info, "waitForUserValidation", "user_confirmed", info.password || undefined);
+      startRegCheckService.call(this, info, "waitForUserValidation", "user_confirmed");
     },
 
     doPickEmail: function(info) {
@@ -85,6 +85,10 @@ BrowserID.Modules.Actions = (function() {
       dialogHelpers.addSecondaryEmail.call(this, info.email, info.password, info.ready);
     },
 
+    doConfirmEmail: function(info) {
+      startRegCheckService.call(this, info, "waitForEmailValidation", "email_confirmed");
+    },
+
     doAuthenticate: function(info) {
       startService("authenticate", info);
     },
@@ -93,16 +97,24 @@ BrowserID.Modules.Actions = (function() {
       startService("required_email", info);
     },
 
-    doForgotPassword: function(info) {
+    doResetPassword: function(info) {
       startService("set_password", _.extend(info, { password_reset: true }));
     },
 
-    doResetPassword: function(info) {
+    doStageResetPassword: function(info) {
       dialogHelpers.resetPassword.call(this, info.email, info.password, info.ready);
     },
 
-    doConfirmEmail: function(info) {
-      startRegCheckService.call(this, info, "waitForEmailValidation", "email_confirmed");
+    doConfirmResetPassword: function(info) {
+      startRegCheckService.call(this, info, "waitForPasswordResetComplete", "staged_address_confirmed");
+    },
+
+    doStageReverifyEmail: function(info) {
+      dialogHelpers.reverifyEmail.call(this, info.email, info.ready);
+    },
+
+    doConfirmReverifyEmail: function(info) {
+      startRegCheckService.call(this, info, "waitForEmailReverifyComplete", "staged_address_confirmed");
     },
 
     doAssertionGenerated: function(info) {
diff --git a/resources/static/pages/js/signup.js b/resources/static/pages/js/signup.js
index d7d4049ba49380eddc94045312655fd0685d1c5f..49677c8e48721f6fb75f3e2a14726716569617be 100644
--- a/resources/static/pages/js/signup.js
+++ b/resources/static/pages/js/signup.js
@@ -80,7 +80,7 @@ BrowserID.signUp = (function() {
 
       if(valid) {
         user.createSecondaryUser(this.emailToStage, pass, function(status) {
-          if(status) {
+          if(status.success) {
             pageHelpers.emailSent(oncomplete && oncomplete.curry(true));
           }
           else {
diff --git a/resources/static/pages/js/start.js b/resources/static/pages/js/start.js
index fdcdcf2f644cdb65cf8b6e335d2192f728bbe3bb..4fa4b83a5e1115f391df2ff709e9e967947edfad 100644
--- a/resources/static/pages/js/start.js
+++ b/resources/static/pages/js/start.js
@@ -26,7 +26,7 @@ $(function() {
       XHRDisableForm = modules.XHRDisableForm,
       Development = modules.Development,
       ANIMATION_TIME = 500,
-      checkCookiePaths = [ "/signin", "/signup", "/forgot", "/add_email_address", "/verify_email_address" ];
+      checkCookiePaths = [ "/signin", "/signup", "/forgot", "/add_email_address", "/confirm", "/verify_email_address" ];
 
 
   function shouldCheckCookies(path) {
@@ -117,6 +117,14 @@ $(function() {
     start(true);
   }
 
+  function verifySecondaryAddress(verifyFunction) {
+    var module = bid.verifySecondaryAddress.create();
+    module.start({
+      token: token,
+      verifyFunction: verifyFunction
+    });
+  }
+
   function start(status) {
     // If cookies are disabled, do not run any of the page specific code and
     // instead just show the error message.
@@ -137,19 +145,22 @@ $(function() {
     else if (path === "/forgot") {
       bid.forgot();
     }
+    // START TRANSITION CODE
+    // add_email_address has been renamed to confirm. Once all outstanding
+    // emails are verified or expired, this can be removed. This change is
+    // scheduled to go into train-2012.07.20
     else if (path === "/add_email_address") {
-      var module = bid.verifySecondaryAddress.create();
-      module.start({
-        token: token,
-        verifyFunction: "verifyEmail"
-      });
+      verifySecondaryAddress("verifyEmail");
+    }
+    // END TRANSITION CODE
+    else if (path === "/confirm") {
+      verifySecondaryAddress("verifyEmail");
     }
     else if (path === "/verify_email_address") {
-      var module = bid.verifySecondaryAddress.create();
-      module.start({
-        token: token,
-        verifyFunction: "verifyUser"
-      });
+      verifySecondaryAddress("verifyUser");
+    }
+    else if (path === "/reset_password") {
+      verifySecondaryAddress("completePasswordReset");
     }
     else if (path === "/about") {
       var module = bid.about.create();
diff --git a/resources/static/test/cases/common/js/network.js b/resources/static/test/cases/common/js/network.js
index 5fac936df6c34fa014b5c2e2cf59d2b36de319d9..7000cf007a253c8277d90095f023326bef838fa4 100644
--- a/resources/static/test/cases/common/js/network.js
+++ b/resources/static/test/cases/common/js/network.js
@@ -17,7 +17,7 @@
 
   var network = BrowserID.Network;
 
-  module("shared/network", {
+  module("common/js/network", {
     setup: function() {
       testHelpers.setup();
     },
@@ -26,6 +26,43 @@
     }
   });
 
+  function testVerificationPending(funcName) {
+    transport.useResult("pending");
+
+    network[funcName]("registered@testuser.com", function(status) {
+      equal(status, "pending");
+      start();
+    }, testHelpers.unexpectedFailure);
+  }
+
+  function testVerificationMustAuth(funcName) {
+    transport.useResult("mustAuth");
+
+    network.checkAuth(function(auth_status) {
+      equal(!!auth_status, false, "user not yet authenticated");
+      network[funcName]("registered@testuser.com", function(status) {
+        equal(status, "mustAuth");
+        network.checkAuth(function(auth_status) {
+          equal(!!auth_status, false, "user not yet authenticated");
+          start();
+        }, testHelpers.unexpectedFailure);
+      }, testHelpers.unexpectedFailure);
+    }, testHelpers.unexpectedFailure);
+  }
+
+  function testVerificationComplete(funcName) {
+    network.withContext(function() {
+      transport.useResult("complete");
+      network[funcName]("registered@testuser.com", function(status) {
+        equal(status, "complete");
+        network.checkAuth(function(auth_level) {
+          equal(auth_level, "password", "user can only be authenticated to password level after verification is complete");
+          start();
+        });
+      }, testHelpers.unexpectedFailure);
+    });
+  }
+
 
   asyncTest("authenticate with valid user", function() {
     network.authenticate(TEST_EMAIL, "testuser", function onSuccess(authenticated) {
@@ -78,7 +115,7 @@
       delayInfo = delay_info;
     });
 
-    var completeInfo
+    var completeInfo;
     mediator.subscribe("xhr_complete", function(msg, complete_info) {
       completeInfo = complete_info;
     });
@@ -376,27 +413,9 @@
     failureCheck(network.addSecondaryEmail, TEST_EMAIL, TEST_PASSWORD, "origin");
   });
 
-  asyncTest("checkEmailRegistration pending", function() {
-    transport.useResult("pending");
-
-    network.checkEmailRegistration("registered@testuser.com", function(status) {
-      equal(status, "pending");
-      start();
-    }, testHelpers.unexpectedFailure);
-  });
-
-  asyncTest("checkEmailRegistration complete", function() {
-    transport.useResult("complete");
-
-    network.checkEmailRegistration("registered@testuser.com", function(status) {
-      equal(status, "complete");
-      start();
-    }, function onFailure() {
-      ok(false);
-      start();
-    });
-
-  });
+  asyncTest("checkEmailRegistration pending", testVerificationPending.curry("checkEmailRegistration"));
+  asyncTest("checkEmailRegistration mustAuth", testVerificationMustAuth.curry("checkEmailRegistration"));
+  asyncTest("checkEmailRegistration complete", testVerificationComplete.curry("checkEmailRegistration"));
 
   asyncTest("checkEmailRegistration with XHR failure", function() {
     failureCheck(network.checkEmailRegistration, TEST_EMAIL);
@@ -488,6 +507,61 @@
     failureCheck(network.requestPasswordReset, TEST_EMAIL, "password", "origin");
   });
 
+  asyncTest("completePasswordReset with valid token, no password required", function() {
+    network.completePasswordReset("token", undefined, function(registered) {
+      ok(registered);
+      start();
+    }, testHelpers.unexpectedFailure);
+  });
+
+  asyncTest("completePasswordReset with valid token, bad password", function() {
+    transport.useResult("badPassword");
+    network.completePasswordReset("token", "password",
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectedXHRFailure);
+  });
+
+  asyncTest("completePasswordReset with valid token, password required", function() {
+    network.completePasswordReset("token", "password", function(registered) {
+      ok(registered);
+      start();
+    }, testHelpers.unexpectedFailure);
+  });
+
+  asyncTest("completePasswordReset with invalid token", function() {
+    transport.useResult("invalid");
+
+    network.completePasswordReset("token", "password", function(registered) {
+      equal(registered, false);
+      start();
+    }, testHelpers.unexpectedFailure);
+  });
+
+  asyncTest("completePasswordReset with XHR failure", function() {
+    failureCheck(network.completePasswordReset, "token", "password");
+  });
+
+  asyncTest("checkPasswordReset pending", testVerificationPending.curry("checkPasswordReset"));
+  asyncTest("checkPasswordReset mustAuth", testVerificationMustAuth.curry("checkPasswordReset"));
+  asyncTest("checkPasswordReset complete", testVerificationComplete.curry("checkPasswordReset"));
+
+
+  asyncTest("requestEmailReverify - true status", function() {
+    network.requestEmailReverify(TEST_EMAIL, "origin", function onSuccess(status) {
+      equal(status, true, "password reset request success");
+      start();
+    }, testHelpers.unexpectedFailure);
+  });
+
+  asyncTest("requestEmailReverify with XHR failure", function() {
+    failureCheck(network.requestEmailReverify, TEST_EMAIL, "origin");
+  });
+
+  asyncTest("checkEmailReverify pending", testVerificationPending.curry("checkEmailReverify"));
+  asyncTest("checkEmailReverify mustAuth", testVerificationMustAuth.curry("checkEmailReverify"));
+  asyncTest("checkEmailReverify complete", testVerificationComplete.curry("checkEmailReverify"));
+
+
   asyncTest("setPassword happy case expects true status", function() {
     network.setPassword("password", function onComplete(status) {
       equal(status, true, "correct status");
diff --git a/resources/static/test/cases/common/js/user.js b/resources/static/test/cases/common/js/user.js
index 585210585e97ad09063006488b9378834322ab08..1911f69a256b1623c871483d2c706c4345c1c3fd 100644
--- a/resources/static/test/cases/common/js/user.js
+++ b/resources/static/test/cases/common/js/user.js
@@ -3,10 +3,12 @@
 /* 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/. */
-var jwcrypto = require("./lib/jwcrypto");
 
 (function() {
-  var bid = BrowserID,
+  "use strict";
+
+  var jwcrypto = require("./lib/jwcrypto"),
+      bid = BrowserID,
       lib = bid.User,
       storage = bid.Storage,
       network = bid.Network,
@@ -17,6 +19,7 @@ var jwcrypto = require("./lib/jwcrypto");
       failureCheck = testHelpers.failureCheck,
       testUndefined = testHelpers.testUndefined,
       testNotUndefined = testHelpers.testNotUndefined,
+      testObjectValuesEqual = testHelpers.testObjectValuesEqual,
       provisioning = bid.Mocks.Provisioning,
       TEST_EMAIL = "testuser@testuser.com";
 
@@ -44,7 +47,7 @@ var jwcrypto = require("./lib/jwcrypto");
 
     // Check for parts of the assertion
     equal(components.payload.aud, testOrigin, "correct audience");
-    var expires = parseInt(components.payload.exp);
+    var expires = parseInt(components.payload.exp, 10);
     ok(typeof expires === "number" && !isNaN(expires), "expiration date is valid");
 
     // this should be based on server time, not local time.
@@ -65,7 +68,7 @@ var jwcrypto = require("./lib/jwcrypto");
     });
   }
 
-  module("shared/user", {
+  module("common/js/user", {
     setup: function() {
       testHelpers.setup();
     },
@@ -155,7 +158,7 @@ var jwcrypto = require("./lib/jwcrypto");
 
   asyncTest("createSecondaryUser success - callback with true status", function() {
     lib.createSecondaryUser(TEST_EMAIL, "password", function(status) {
-      ok(status, "user created");
+      ok(status.success, "user created");
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
@@ -164,7 +167,10 @@ var jwcrypto = require("./lib/jwcrypto");
     xhr.useResult("throttle");
 
     lib.createSecondaryUser(TEST_EMAIL, "password", function(status) {
-      equal(status, false, "user creation refused");
+      testObjectValuesEqual(status, {
+        success: false,
+        reason: "throttle"
+      });
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
@@ -412,13 +418,17 @@ var jwcrypto = require("./lib/jwcrypto");
 
   asyncTest("verifyUser with a good token", function() {
     storage.setReturnTo(testOrigin);
+    storage.addSecondaryEmail(TEST_EMAIL, { verified: false });
 
     lib.verifyUser("token", "password", function onSuccess(info) {
 
-      ok(info.valid, "token was valid");
-      equal(info.email, TEST_EMAIL, "email part of info");
-      equal(info.returnTo, testOrigin, "returnTo in info");
+      testObjectValuesEqual(info, {
+        valid: true,
+        email: TEST_EMAIL,
+        returnTo: testOrigin
+      });
       equal(storage.getReturnTo(), "", "initiating origin was removed");
+      equal(storage.getEmail(TEST_EMAIL).verified, true, "email marked as verified");
 
       start();
     }, testHelpers.unexpectedXHRFailure);
@@ -516,6 +526,99 @@ var jwcrypto = require("./lib/jwcrypto");
     failureCheck(lib.requestPasswordReset, "registered@testuser.com", "password");
   });
 
+  asyncTest("completePasswordReset with a good token", function() {
+    storage.addSecondaryEmail(TEST_EMAIL, { verified: false });
+    storage.setReturnTo(testOrigin);
+
+    lib.completePasswordReset("token", "password", function onSuccess(info) {
+      testObjectValuesEqual(info, {
+        valid: true,
+        email: TEST_EMAIL,
+        returnTo: testOrigin,
+      });
+
+      equal(storage.getReturnTo(), "", "initiating origin was removed");
+      equal(storage.getEmail(TEST_EMAIL).verified, true, "email now marked as verified");
+
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("completePasswordReset with a bad token", function() {
+    xhr.useResult("invalid");
+
+    lib.completePasswordReset("token", "password", function onSuccess(info) {
+      equal(info.valid, false, "bad token calls onSuccess with a false validity");
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("completePasswordReset with an XHR failure", function() {
+    xhr.useResult("ajaxError");
+
+    lib.completePasswordReset(
+      "token",
+      "password",
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectedXHRFailure
+    );
+  });
+
+  asyncTest("requestEmailReverify with owned verified email - false status", function() {
+    storage.addSecondaryEmail(TEST_EMAIL, { verified: true });
+
+    var returnTo = "http://samplerp.org";
+    lib.setReturnTo(returnTo);
+    lib.requestEmailReverify(TEST_EMAIL, function(status) {
+      testObjectValuesEqual(status, {
+        success: false,
+        reason: "verified_email"
+      });
+
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("requestEmailReverify with owned unverified email - false status", function() {
+    storage.addSecondaryEmail(TEST_EMAIL, { verified: false });
+
+    var returnTo = "http://samplerp.org";
+    lib.setReturnTo(returnTo);
+    lib.requestEmailReverify(TEST_EMAIL, function(status) {
+      equal(status.success, true, "password reset for known user");
+      equal(storage.getReturnTo(), returnTo, "RP URL is stored for verification");
+
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("requestEmailReverify with unowned email - false status, invalid_user", function() {
+    lib.requestEmailReverify(TEST_EMAIL, function(status) {
+      testObjectValuesEqual(status, {
+        success: false,
+        reason: "invalid_email"
+      });
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("requestEmailReverify owned email with throttle - false status, throttle", function() {
+    xhr.useResult("throttle");
+    storage.addSecondaryEmail(TEST_EMAIL, { verified: false });
+
+    lib.requestEmailReverify(TEST_EMAIL, function(status) {
+      testObjectValuesEqual(status, {
+        success: false,
+        reason: "throttle"
+      });
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("requestEmailReverify with XHR failure", function() {
+    storage.addSecondaryEmail(TEST_EMAIL, { verified: false });
+    failureCheck(lib.requestEmailReverify, TEST_EMAIL);
+  });
 
   asyncTest("authenticate with valid credentials, also syncs email with server", function() {
     lib.authenticate(TEST_EMAIL, "testuser", function(authenticated) {
@@ -793,12 +896,15 @@ var jwcrypto = require("./lib/jwcrypto");
 
   asyncTest("verifyEmail with a good token - callback with email, returnTo, valid", function() {
     storage.setReturnTo(testOrigin);
+    storage.addSecondaryEmail(TEST_EMAIL, { verified: false });
     lib.verifyEmail("token", "password", function onSuccess(info) {
-
-      ok(info.valid, "token was valid");
-      equal(info.email, TEST_EMAIL, "email part of info");
-      equal(info.returnTo, testOrigin, "returnTo in info");
+      testObjectValuesEqual(info, {
+        valid: true,
+        email: TEST_EMAIL,
+        returnTo: testOrigin
+      });
       equal(storage.getReturnTo(), "", "initiating returnTo was removed");
+      equal(storage.getEmail(TEST_EMAIL).verified, true, "email now marked as verified");
 
       start();
     }, testHelpers.unexpectedXHRFailure);
@@ -945,12 +1051,16 @@ var jwcrypto = require("./lib/jwcrypto");
     }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("syncEmails with one to refresh", function() {
-    storage.addEmail(TEST_EMAIL, {pub: pubkey, cert: random_cert});
+  asyncTest("syncEmails with one to update", function() {
+    // verified is set to false here,  the mock for list_emails has verified
+    // set to true.  If emails are being updated, verified will be set to true
+    // whenever syncEmails is complete.
+    storage.addEmail(TEST_EMAIL, {pub: pubkey, cert: random_cert, verified: false});
 
     lib.syncEmails(function onSuccess() {
       var identities = lib.getStoredEmailKeypairs();
       ok(TEST_EMAIL in identities, "refreshed key is synced");
+      equal(identities[TEST_EMAIL].verified, true, "verified was correctly updated");
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
@@ -1262,7 +1372,6 @@ var jwcrypto = require("./lib/jwcrypto");
           start();
         }, testHelpers.expectedXHRFailure);
       }, testHelpers.unexpectedXHRFailure);
-      xhr.useResult
     }, testHelpers.unexpectedXHRFailure);
   });
 
diff --git a/resources/static/test/cases/dialog/js/misc/state.js b/resources/static/test/cases/dialog/js/misc/state.js
index d6535638fd817d6385d7950077102dfbffd5fb72..eabca0b5089db9cb4676578c79593e42fb702ec4 100644
--- a/resources/static/test/cases/dialog/js/misc/state.js
+++ b/resources/static/test/cases/dialog/js/misc/state.js
@@ -42,6 +42,36 @@
     }
   }
 
+  function testVerifyStagedAddress(startMessage, verifyScreenAction) {
+    // start with a site name to ensure the site name is passed to the
+    // verifyScreenAction.
+    mediator.publish("start", { siteName: "Unit Test Site" });
+    mediator.publish(startMessage, {
+      email: TEST_EMAIL
+    });
+
+    testActionStarted(verifyScreenAction, {
+      email: TEST_EMAIL,
+      siteName: "Unit Test Site"
+    });
+
+    // At this point the user should be displayed the "go confirm your address"
+    // screen.  Simulate the user completing the verification transaction.
+
+    mediator.subscribe("email_chosen", function(msg, info) {
+      equal(info.email, TEST_EMAIL, "email_chosen triggered with the correct email");
+      start();
+    });
+
+    // staged_address_confirmed means the user has confirmed their email and the dialog
+    // has received the "complete" message from the polling function, and all
+    // addresses are synced.  Add the test email and make sure the email_chosen
+    // message is triggered.
+    storage.addSecondaryEmail(TEST_EMAIL, { unverified: true });
+    mediator.publish("staged_address_confirmed");
+  }
+
+
   function createMachine() {
     machine = bid.State.create();
     actions = new ActionsMock();
@@ -61,7 +91,7 @@
     });
   }
 
-  module("resources/state", {
+  module("dialog/js/misc/state", {
     setup: function() {
       testHelpers.setup();
       createMachine();
@@ -128,11 +158,11 @@
     equal(actions.info.doStageEmail.email, TEST_EMAIL, "correct email sent to doStageEmail");
   });
 
-  test("password_set for reset password - call doResetPassword with correct email", function() {
+  test("password_set for reset password - call doStageResetPassword with correct email", function() {
     mediator.publish("forgot_password", { email: TEST_EMAIL });
     mediator.publish("password_set");
 
-    equal(actions.info.doResetPassword.email, TEST_EMAIL, "correct email sent to doResetPassword");
+    equal(actions.info.doStageResetPassword.email, TEST_EMAIL, "correct email sent to doStageResetPassword");
   });
 
   test("start - RPInfo always started", function() {
@@ -145,17 +175,8 @@
     ok(actions.info.doRPInfo.privacyPolicy, "doRPInfo called with privacyPolicy set");
   });
 
-  test("user_staged - call doConfirmUser", function() {
-    mediator.publish("user_staged", { email: TEST_EMAIL });
-
-    equal(actions.info.doConfirmUser.email, TEST_EMAIL, "waiting for email confirmation for testuser@testuser.com");
-  });
-
-  test("user_staged with required email - call doConfirmUser with required = true", function() {
-    mediator.publish("start", { requiredEmail: TEST_EMAIL });
-    mediator.publish("user_staged", { email: TEST_EMAIL });
-
-    equal(actions.info.doConfirmUser.required, true, "doConfirmUser called with required flag");
+  asyncTest("user_staged - call doConfirmUser", function() {
+    testVerifyStagedAddress("user_staged", "doConfirmUser");
   });
 
   test("user_confirmed - redirect to email_chosen", function() {
@@ -175,17 +196,8 @@
     }
   });
 
-  test("email_staged - call doConfirmEmail", function() {
-    mediator.publish("email_staged", { email: TEST_EMAIL });
-
-    equal(actions.info.doConfirmEmail.required, false, "doConfirmEmail called without required flag");
-  });
-
-  test("email_staged with required email - call doConfirmEmail with required = true", function() {
-    mediator.publish("start", { requiredEmail: TEST_EMAIL });
-    mediator.publish("email_staged", { email: TEST_EMAIL });
-
-    equal(actions.info.doConfirmEmail.required, true, "doConfirmEmail called with required flag");
+  asyncTest("email_staged - call doConfirmEmail", function() {
+    testVerifyStagedAddress("email_staged", "doConfirmEmail");
   });
 
   asyncTest("primary_user with already provisioned primary user - redirect to primary_user_ready", function() {
@@ -272,51 +284,20 @@
     mediator.publish("authenticated", { email: TEST_EMAIL });
   });
 
-  test("forgot_password - call doForgotPassword with correct options", function() {
+  test("forgot_password - call doResetPassword with correct options", function() {
     mediator.publish("start", { privacyPolicy: "priv.html", termsOfService: "tos.html" });
     mediator.publish("forgot_password", {
       email: TEST_EMAIL,
       requiredEmail: true
     });
-    testActionStarted("doForgotPassword", { email: TEST_EMAIL, requiredEmail: true });
+    testActionStarted("doResetPassword", { email: TEST_EMAIL, requiredEmail: true });
   });
 
-  test("password_reset to user_confirmed - call doUserStaged then doEmailConfirmed", function() {
-    // password_reset indicates the user has verified that they want to reset
-    // their password.
-    mediator.publish("password_reset", {
-      email: TEST_EMAIL
-    });
-    equal(actions.info.doConfirmUser.email, TEST_EMAIL, "doConfirmUser with the correct email");
-
-    // At this point the user should be displayed the "go confirm your address"
-    // screen.
-
-    // user_confirmed means the user has confirmed their email and the dialog
-    // has received the "complete" message from /wsapi/user_creation_status.
-    try {
-      mediator.publish("user_confirmed");
-    } catch(e) {
-      // Exception is expected because as part of the user confirmation
-      // process, before user_confirmed is called, email addresses are synced.
-      // Addresses are not synced in this test.
-      equal(e.toString(), "invalid email", "expected failure");
-    }
+  asyncTest("reset_password_staged to staged_address_confirmed - call doConfirmResetPassword then doEmailConfirmed", function() {
+    testVerifyStagedAddress("reset_password_staged", "doConfirmResetPassword");
   });
 
 
-  test("cancel password_reset flow - go two steps back", function() {
-    // we want to skip the "verify" screen of reset password and instead go two
-    // screens back.  Do do this, we are simulating the steps necessary to get
-    // to the password_reset flow.
-    mediator.publish("authenticate");
-    mediator.publish("forgot_password", undefined, { email: TEST_EMAIL });
-    mediator.publish("password_reset");
-    actions.info.doAuthenticate = {};
-    mediator.publish("cancel_state");
-    equal(actions.info.doAuthenticate.email, TEST_EMAIL, "authenticate called with the correct email");
-  });
-
   asyncTest("assertion_generated with null assertion - redirect to pick_email", function() {
     mediator.subscribe("pick_email", function() {
       ok(true, "redirect to pick_email");
@@ -433,15 +414,14 @@
     testActionStarted("doAddEmail");
   });
 
-  asyncTest("email_chosen with secondary email, user must authenticate - call doAuthenticateWithRequiredEmail", function() {
-    var email = TEST_EMAIL;
-    storage.addEmail(email, { type: "secondary" });
+  asyncTest("email_chosen with verified secondary email, user must authenticate - call doAuthenticateWithRequiredEmail", function() {
+    storage.addSecondaryEmail(TEST_EMAIL, { verified: true });
 
     xhr.setContextInfo("auth_level", "assertion");
 
     mediator.publish("start", { privacyPolicy: "priv.html", termsOfService: "tos.html" });
     mediator.publish("email_chosen", {
-      email: email,
+      email: TEST_EMAIL,
       complete: function() {
         testActionStarted("doAuthenticateWithRequiredEmail", { siteTOSPP: false });
         start();
@@ -449,8 +429,8 @@
     });
   });
 
-  asyncTest("email_chosen with secondary email, user authenticated to secondary - redirect to email_valid_and_ready", function() {
-    storage.addEmail(TEST_EMAIL, { type: "secondary" });
+  asyncTest("email_chosen with verified secondary email, user authenticated to secondary - redirect to email_valid_and_ready", function() {
+    storage.addSecondaryEmail(TEST_EMAIL, { verified: true });
     xhr.setContextInfo("auth_level", "password");
 
     mediator.subscribe("email_valid_and_ready", function(msg, info) {
@@ -463,6 +443,28 @@
     });
   });
 
+  function testReverifyEmailChosen(auth_level) {
+    storage.addSecondaryEmail(TEST_EMAIL, { verified: false });
+    xhr.setContextInfo("auth_level", auth_level);
+
+    mediator.subscribe("stage_reverify_email", function(msg, info) {
+      equal(info.email, TEST_EMAIL, "correctly redirected to stage_reverify_email with correct email");
+      start();
+    });
+
+    mediator.publish("email_chosen", {
+      email: TEST_EMAIL
+    });
+  }
+
+  asyncTest("email_chosen with unverified secondary email, user authenticated to secondary - redirect to stage_reverify_email", function() {
+    testReverifyEmailChosen("password");
+  });
+
+  asyncTest("email_chosen with unverified secondary email, user authenticated to primary - redirect to stage_reverify_email", function() {
+    testReverifyEmailChosen("assertion");
+  });
+
   test("email_chosen with primary email - call doProvisionPrimaryUser", function() {
     // If the email is a primary, throw the user down the primary flow.
     // Doing so will catch cases where the primary certificate is expired
@@ -544,6 +546,15 @@
     });
   });
 
+  test("stage_reverify_email - call doStageReverifyEmail", function() {
+    mediator.publish("stage_reverify_email", { email: TEST_EMAIL });
+    testActionStarted("doStageReverifyEmail", { email: TEST_EMAIL });
+  });
+
+  test("reverify_email_staged - call doConfirmReverifyEmail", function() {
+    testVerifyStagedAddress("reverify_email_staged", "doConfirmReverifyEmail");
+  });
+
   asyncTest("window_unload - set the final KPIs", function() {
     mediator.subscribe("kpi_data", function(msg, data) {
       testHelpers.testKeysInObject(data, [
diff --git a/resources/static/test/cases/dialog/js/modules/actions.js b/resources/static/test/cases/dialog/js/modules/actions.js
index 3f98fc81b8d968879c1c34738a00b4eab5500111..7c44e045bcdeaa795310336ccc2db0452eea215b 100644
--- a/resources/static/test/cases/dialog/js/modules/actions.js
+++ b/resources/static/test/cases/dialog/js/modules/actions.js
@@ -8,6 +8,7 @@
 
   var bid = BrowserID,
       user = bid.User,
+      storage = bid.Storage,
       controller,
       el,
       testHelpers = bid.TestHelpers,
@@ -34,7 +35,29 @@
     });
   }
 
-  module("controllers/actions", {
+  function testStageAddress(actionName, expectedMessage) {
+    createController({
+      ready: function() {
+        var message,
+            email;
+
+        testHelpers.register(expectedMessage, function(msg, info) {
+          message = msg;
+          email = info.email;
+        });
+
+        controller[actionName]({ email: TEST_EMAIL, password: "password", ready: function(status) {
+          equal(status, true, "correct status");
+          equal(message, expectedMessage, "correct message triggered");
+          equal(email, TEST_EMAIL, "address successfully staged");
+          start();
+        }});
+      }
+    });
+  }
+
+
+  module("dialog/js/modules/actions", {
     setup: function() {
       testHelpers.setup();
     },
@@ -74,39 +97,50 @@
       "primary_user_provisioned");
   });
 
+  asyncTest("doStageUser with successful creation - trigger user_staged", function() {
+    testStageAddress("doStageUser", "user_staged");
+  });
+
   asyncTest("doConfirmUser - start the check_registration service", function() {
     testActionStartsModule("doConfirmUser", {email: TEST_EMAIL, siteName: "Unit Test Site"},
       "check_registration");
   });
 
+  asyncTest("doStageEmail with successful staging - trigger email_staged", function() {
+    testStageAddress("doStageEmail", "email_staged");
+  });
+
   asyncTest("doConfirmEmail - start the check_registration service", function() {
     testActionStartsModule("doConfirmEmail", {email: TEST_EMAIL, siteName: "Unit Test Site"},
       "check_registration");
   });
 
-  asyncTest("doGenerateAssertion - start the generate_assertion service", function() {
-    testActionStartsModule('doGenerateAssertion', { email: TEST_EMAIL }, "generate_assertion");
+  asyncTest("doResetPassword - call the set_password controller with reset_password true", function() {
+    testActionStartsModule('doResetPassword', { email: TEST_EMAIL }, "set_password");
   });
 
-  asyncTest("doStageUser with successful creation - trigger user_staged", function() {
-    createController({
-      ready: function() {
-        var email;
-        testHelpers.register("user_staged", function(msg, info) {
-          email = info.email;
-        });
+  asyncTest("doStageResetPassword - trigger reset_password_staged", function() {
+    testStageAddress("doStageResetPassword", "reset_password_staged");
+  });
 
-        controller.doStageUser({ email: TEST_EMAIL, password: "password", ready: function(status) {
-          equal(status, true, "correct status");
-          equal(email, TEST_EMAIL, "user successfully staged");
-          start();
-        }});
-      }
-    });
+  asyncTest("doConfirmResetPassword - start the check_registration service", function() {
+    testActionStartsModule("doConfirmResetPassword", {email: TEST_EMAIL, siteName: "Unit Test Site"},
+      "check_registration");
+  });
+
+  asyncTest("doStageReverifyEmail - trigger reverify_email_staged", function() {
+
+    storage.addSecondaryEmail(TEST_EMAIL, { verified: false });
+    testStageAddress("doStageReverifyEmail", "reverify_email_staged");
   });
 
-  asyncTest("doForgotPassword - call the set_password controller with reset_password true", function() {
-    testActionStartsModule('doForgotPassword', { email: TEST_EMAIL }, "set_password");
+  asyncTest("doConfirmReverifyEmail - start the check_registration service", function() {
+    testActionStartsModule("doConfirmReverifyEmail", {email: TEST_EMAIL, siteName: "Unit Test Site"},
+      "check_registration");
+  });
+
+  asyncTest("doGenerateAssertion - start the generate_assertion service", function() {
+    testActionStartsModule('doGenerateAssertion', { email: TEST_EMAIL }, "generate_assertion");
   });
 
   asyncTest("doRPInfo - start the rp_info service", function() {
diff --git a/resources/static/test/cases/pages/js/verify_secondary_address.js b/resources/static/test/cases/pages/js/verify_secondary_address.js
index f15227bc0934937982552abb941804c3fe69463f..fcbbe88392b2d1f792bc76d9e1f1a3ac5d7b74d1 100644
--- a/resources/static/test/cases/pages/js/verify_secondary_address.js
+++ b/resources/static/test/cases/pages/js/verify_secondary_address.js
@@ -25,7 +25,7 @@
   module("pages/verify_secondary_address", {
     setup: function() {
       testHelpers.setup();
-      bid.Renderer.render("#page_head", "site/add_email_address", {});
+      bid.Renderer.render("#page_head", "site/confirm", {});
       $(".siteinfo,.password_entry").hide();
     },
     teardown: function() {
diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js
index 95f6b1d192ce8d0bb9ddd7063a796daa51b0af63..99116c22ffabfc7240808018866584c1226a2b7e 100644
--- a/resources/static/test/mocks/xhr.js
+++ b/resources/static/test/mocks/xhr.js
@@ -53,15 +53,45 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/cert_key valid": random_cert,
       "post /wsapi/cert_key invalid": undefined,
       "post /wsapi/cert_key ajaxError": undefined,
-      "post /wsapi/complete_email_addition valid": { success: true },
-      "post /wsapi/complete_email_addition badPassword": 401,
-      "post /wsapi/complete_email_addition invalid": { success: false },
-      "post /wsapi/complete_email_addition ajaxError": undefined,
+      "post /wsapi/complete_email_confirmation valid": { success: true },
+      "post /wsapi/complete_email_confirmation badPassword": 401,
+      "post /wsapi/complete_email_confirmation invalid": { success: false },
+      "post /wsapi/complete_email_confirmation ajaxError": undefined,
       "post /wsapi/stage_user unknown_secondary": { success: true },
       "post /wsapi/stage_user valid": { success: true },
       "post /wsapi/stage_user invalid": { success: false },
       "post /wsapi/stage_user throttle": 429,
       "post /wsapi/stage_user ajaxError": undefined,
+
+      "post /wsapi/stage_reset unknown_secondary": { success: true },
+      "post /wsapi/stage_reset valid": { success: true },
+      "post /wsapi/stage_reset invalid": { success: false },
+      "post /wsapi/stage_reset throttle": 429,
+      "post /wsapi/stage_reset ajaxError": undefined,
+
+      "post /wsapi/complete_reset valid": { success: true },
+      "post /wsapi/complete_reset badPassword": 401,
+      "post /wsapi/complete_reset invalid": { success: false },
+      "post /wsapi/complete_reset ajaxError": undefined,
+
+      "get /wsapi/password_reset_status?email=registered%40testuser.com pending": { status: "pending" },
+      "get /wsapi/password_reset_status?email=registered%40testuser.com complete": { status: "complete", userid: 4 },
+      "get /wsapi/password_reset_status?email=registered%40testuser.com mustAuth": { status: "mustAuth" },
+      "get /wsapi/password_reset_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" },
+      "get /wsapi/password_reset_status?email=registered%40testuser.com ajaxError": undefined,
+
+      "post /wsapi/stage_reverify unknown_secondary": { success: true },
+      "post /wsapi/stage_reverify valid": { success: true },
+      "post /wsapi/stage_reverify invalid": { success: false },
+      "post /wsapi/stage_reverify throttle": 429,
+      "post /wsapi/stage_reverify ajaxError": undefined,
+
+      "get /wsapi/email_reverify_status?email=registered%40testuser.com pending": { status: "pending" },
+      "get /wsapi/email_reverify_status?email=registered%40testuser.com complete": { status: "complete", userid: 4 },
+      "get /wsapi/email_reverify_status?email=registered%40testuser.com mustAuth": { status: "mustAuth" },
+      "get /wsapi/email_reverify_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" },
+      "get /wsapi/email_reverify_status?email=registered%40testuser.com ajaxError": undefined,
+
       "get /wsapi/user_creation_status?email=registered%40testuser.com pending": { status: "pending" },
       "get /wsapi/user_creation_status?email=registered%40testuser.com complete": { status: "complete", userid: 4 },
       "get /wsapi/user_creation_status?email=registered%40testuser.com mustAuth": { status: "mustAuth" },
@@ -77,6 +107,9 @@ BrowserID.Mocks.xhr = (function() {
       "get /wsapi/have_email?email=registered%40testuser.com valid": { email_known: true },
       "get /wsapi/have_email?email=registered%40testuser.com throttle": { email_known: true },
       "get /wsapi/have_email?email=registered%40testuser.com ajaxError": undefined,
+      "get /wsapi/have_email?email=testuser%40testuser.com valid": { email_known: true },
+      "get /wsapi/have_email?email=testuser%40testuser.com throttle": { email_known: true },
+      "get /wsapi/have_email?email=testuser%40testuser.com ajaxError": undefined,
       "get /wsapi/have_email?email=unregistered%40testuser.com valid": { email_known: false },
       "get /wsapi/have_email?email=unregistered%40testuser.com primary": { email_known: false },
       "post /wsapi/remove_email valid": { success: true },
@@ -98,7 +131,8 @@ BrowserID.Mocks.xhr = (function() {
       "get /wsapi/email_addition_status?email=registered%40testuser.com mustAuth": { status: "mustAuth" },
       "get /wsapi/email_addition_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" },
       "get /wsapi/email_addition_status?email=registered%40testuser.com ajaxError": undefined,
-      "get /wsapi/list_emails valid": {"testuser@testuser.com":{ type: "secondary" }},
+      "get /wsapi/list_emails valid": {"testuser@testuser.com":{ type: "secondary", verified: true }},
+      "get /wsapi/list_emails unverified": {"testuser@testuser.com":{ type: "secondary", verified: false }},
       //"get /wsapi/list_emails known_secondary": {"registered@testuser.com":{ type: "secondary" }},
       "get /wsapi/list_emails primary": {"testuser@testuser.com": { type: "primary" }},
       "get /wsapi/list_emails multiple": {"testuser@testuser.com":{}, "testuser2@testuser.com":{}},
diff --git a/resources/views/add_email_address.ejs b/resources/views/confirm.ejs
similarity index 100%
rename from resources/views/add_email_address.ejs
rename to resources/views/confirm.ejs
diff --git a/tests/cache-header-tests.js b/tests/cache-header-tests.js
index 9649791ba48c1ae17cb9902af3e1822dd77dd40e..da97f16712a38a3f8288bf5ce373fb13daccf17d 100755
--- a/tests/cache-header-tests.js
+++ b/tests/cache-header-tests.js
@@ -132,6 +132,7 @@ suite.addBatch({
   '/privacy': hasProperCacheHeaders('/privacy'),
   '/verify_email_address': hasProperCacheHeaders('/verify_email_address'),
   '/add_email_address': hasProperCacheHeaders('/add_email_address'),
+  '/confirm': hasProperCacheHeaders('/confirm'),
 //  '/pk': hasProperCacheHeaders('/pk'),
 //  '/.well-known/browserid': hasProperCacheHeaders('/.well-known/browserid')
 });
diff --git a/tests/db-test.js b/tests/db-test.js
index 6c8ec1ea54b157fb05f29e74918049d7fd4cead2..811f5cfb6ec27313c02335f02b684fd2fd0d8203 100755
--- a/tests/db-test.js
+++ b/tests/db-test.js
@@ -125,7 +125,7 @@ suite.addBatch({
 suite.addBatch({
   "upon receipt of a secret": {
     topic: function() {
-      db.gotVerificationSecret(secret, this.callback);
+      db.completeCreateUser(secret, this.callback);
     },
     "gotVerificationSecret completes without error": function (err, r) {
       assert.isNull(err);
@@ -200,7 +200,10 @@ suite.addBatch({
     },
     "then staging an email": {
       topic: function(err, uid) {
-        db.stageEmail(uid, 'lloyd@somewhe.re', 'biglonghashofapassword', this.callback);
+        // do not supply a password here.  Email addition only supplies a password
+        // in the case it's the addition of a secondary address to an account with
+        // only primaries.
+        db.stageEmail(uid, 'lloyd@somewhe.re', undefined, this.callback);
       },
       "yields a valid secret": function(err, secret) {
         assert.isNull(err);
@@ -215,7 +218,7 @@ suite.addBatch({
         "makes it visible via isStaged": function(sekret, r) { assert.isTrue(r); },
         "lets you verify it": {
           topic: function(secret, r) {
-            db.gotVerificationSecret(secret, this.callback);
+            db.completeConfirmEmail(secret, this.callback);
           },
           "successfully": function(err, r) {
             assert.isNull(err);
@@ -233,6 +236,17 @@ suite.addBatch({
               assert.isNull(err);
               assert.isFalse(r);
             }
+          },
+          "and user's password": {
+            topic: function() {
+              var self = this;
+              db.emailToUID('lloyd@nowhe.re', function(err, uid) {
+                db.checkAuth(uid, self.callback);
+              });
+            },
+            "is still populated": function(err, hash) {
+              assert.strictEqual(hash, "biglonghashofapassword");
+            }
           }
         }
       }
diff --git a/tests/email-throttling-test.js b/tests/email-throttling-test.js
index 9b7e4560cfe8149e06d57272d946c9e042d3ee5d..5b7611572f19b74c96b2dee964f79bc6980c01e2 100755
--- a/tests/email-throttling-test.js
+++ b/tests/email-throttling-test.js
@@ -112,7 +112,7 @@ suite.addBatch({
 suite.addBatch({
   "and when we attempt to finish adding the email address": {
     topic: function() {
-      wsapi.post('/wsapi/complete_email_addition', { token: token }).call(this);
+      wsapi.post('/wsapi/complete_email_confirmation', { token: token }).call(this);
     },
     "it works swimmingly": function(err, r) {
       assert.equal(r.code, 200);
diff --git a/tests/forgotten-email-test.js b/tests/forgotten-email-test.js
deleted file mode 100755
index c2e6aa3823be90f8c8093e013cc5697f09d6e67e..0000000000000000000000000000000000000000
--- a/tests/forgotten-email-test.js
+++ /dev/null
@@ -1,256 +0,0 @@
-#!/usr/bin/env node
-
-/* 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/. */
-
-require('./lib/test_env.js');
-
-const assert = require('assert'),
-vows = require('vows'),
-start_stop = require('./lib/start-stop.js'),
-wsapi = require('./lib/wsapi.js'),
-email = require('../lib/email.js');
-
-var suite = vows.describe('forgotten-email');
-
-start_stop.addStartupBatches(suite);
-
-// every time a new token is sent out, let's update the global
-// var 'token'
-var token = undefined;
-
-// create a new account via the api with (first address)
-suite.addBatch({
-  "staging an account": {
-    topic: wsapi.post('/wsapi/stage_user', {
-      email: 'first@fakeemail.com',
-      pass: 'firstfakepass',
-      site:'http://localhost:123'
-    }),
-    "works": function(err, r) {
-      assert.strictEqual(r.code, 200);
-    }
-  }
-});
-
-// wait for the token
-suite.addBatch({
-  "a token": {
-    topic: function() {
-      start_stop.waitForToken(this.callback);
-    },
-    "is obtained": function (t) {
-      assert.strictEqual(typeof t, 'string');
-      token = t;
-    }
-  }
-});
-
-suite.addBatch({
-  "create first account": {
-    topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
-    },
-    "account created": function(err, r) {
-      assert.equal(r.code, 200);
-      assert.strictEqual(true, JSON.parse(r.body).success);
-      token = undefined;
-    }
-  }
-});
-
-suite.addBatch({
-  "email created": {
-    topic: wsapi.get('/wsapi/user_creation_status', { email: 'first@fakeemail.com' } ),
-    "should exist": function(err, r) {
-      assert.strictEqual(r.code, 200);
-      assert.strictEqual(JSON.parse(r.body).status, "complete");
-    }
-  }
-});
-
-// add a new email address to the account (second address)
-suite.addBatch({
-  "add a new email address to our account": {
-    topic: wsapi.post('/wsapi/stage_email', {
-      email: 'second@fakeemail.com',
-      site:'https://fakesite.foobar.bizbaz.uk'
-    }),
-    "works": function(err, r) {
-      assert.strictEqual(r.code, 200);
-    }
-  }
-});
-
-// wait for the token
-suite.addBatch({
-  "a token": {
-    topic: function() {
-      start_stop.waitForToken(this.callback);
-    },
-    "is obtained": function (t) {
-      assert.strictEqual(typeof t, 'string');
-      token = t;
-    }
-  }
-});
-
-// confirm second email email address to the account
-suite.addBatch({
-  "create second account": {
-    topic: function() {
-      wsapi.post('/wsapi/complete_email_addition', { token: token }).call(this);
-    },
-    "account created": function(err, r) {
-      assert.equal(r.code, 200);
-      assert.strictEqual(JSON.parse(r.body).success, true);
-      token = undefined;
-    }
-  }
-});
-
-// verify now both email addresses are known
-suite.addBatch({
-  "first email exists": {
-    topic: wsapi.get('/wsapi/have_email', { email: 'first@fakeemail.com' }),
-    "should exist": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).email_known, true);
-    }
-  },
-  "second email exists": {
-    topic: wsapi.get('/wsapi/have_email', { email: 'second@fakeemail.com' }),
-    "should exist": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).email_known, true);
-    }
-  },
-  "a random email doesn't exist": {
-    topic: wsapi.get('/wsapi/have_email', { email: 'third@fakeemail.com' }),
-    "shouldn't exist": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).email_known, false);
-    }
-  }
-});
-
-// Run the "forgot_email" flow with first address.  This is really
-// just re-registering the user.
-suite.addBatch({
-  "re-stage first account": {
-    topic: wsapi.post('/wsapi/stage_user', {
-      email: 'first@fakeemail.com',
-      pass: 'secondfakepass',
-      site:'https://otherfakesite.com'
-    }),
-    "works": function(err, r) {
-      assert.strictEqual(r.code, 200);
-    }
-  }
-});
-
-// wait for the token
-suite.addBatch({
-  "a token": {
-    topic: function() {
-      start_stop.waitForToken(this.callback);
-    },
-    "is obtained": function (t) {
-      assert.strictEqual(typeof t, 'string');
-      token = t;
-    }
-  }
-});
-
-// verify that the old email address + password combinations are still
-// valid (this is so *until* someone clicks through)
-suite.addBatch({
-  "first email works": {
-    topic: wsapi.post('/wsapi/authenticate_user', {
-      email: 'first@fakeemail.com',
-      pass: 'firstfakepass',
-      ephemeral: false
-    }),
-    "should work": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).success, true);
-    }
-  },
-  "second email works": {
-    topic: wsapi.post('/wsapi/authenticate_user', {
-      email: 'second@fakeemail.com',
-      pass: 'firstfakepass',
-      ephemeral: false
-    }),
-    "should work": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).success, true);
-    }
-  }
-});
-
-// now let's complete the re-registration of first email address
-suite.addBatch({
-  "re-create first email address": {
-    topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
-    },
-    "account created": function(err, r) {
-      assert.equal(r.code, 200);
-      assert.strictEqual(JSON.parse(r.body).success, true);
-    }
-  }
-});
-
-// now we should be able to sign into the first email address with the first
-// password, and all other combinations should fail
-suite.addBatch({
-  "first email, first pass bad": {
-    topic: wsapi.post('/wsapi/authenticate_user', {
-      email: 'first@fakeemail.com',
-      pass: 'firstfakepass',
-      ephemeral: false
-    }),
-    "shouldn't work": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).success, false);
-    }
-  },
-  "first email, second pass good": {
-    topic: wsapi.post('/wsapi/authenticate_user', {
-      email: 'first@fakeemail.com',
-      pass: 'secondfakepass',
-      ephemeral: false
-    }),
-    "should work": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).success, true);
-    }
-  },
-  "logout": {
-    topic: wsapi.post('/wsapi/logout', {}),
-    "should work": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).success, true);
-    }
-  },
-  "second email, first pass good": {
-    topic: wsapi.post('/wsapi/authenticate_user', {
-      email: 'second@fakeemail.com',
-      pass: 'firstfakepass',
-      ephemeral: false
-    }),
-    "should work": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).success, true);
-    }
-  },
-  "second email, second pass bad": {
-    topic: wsapi.post('/wsapi/authenticate_user', {
-      email: 'second@fakeemail.com',
-      pass: 'secondfakepass',
-      ephemeral: false
-    }),
-    "shouldn' work": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).success, false);
-    }
-  },
-});
-
-start_stop.addShutdownBatches(suite);
-
-// run or export the suite.
-if (process.argv[1] === __filename) suite.run();
-else suite.export(module);
diff --git a/tests/forgotten-pass-test.js b/tests/forgotten-pass-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..7e9aae487b38946332a3096323207edc3ad5e329
--- /dev/null
+++ b/tests/forgotten-pass-test.js
@@ -0,0 +1,465 @@
+#!/usr/bin/env node
+
+/* 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/. */
+
+require('./lib/test_env.js');
+
+const assert = require('assert'),
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js'),
+email = require('../lib/email.js'),
+jwcrypto = require('jwcrypto');
+
+var suite = vows.describe('forgotten-email');
+
+// algs
+require("jwcrypto/lib/algs/ds");
+require("jwcrypto/lib/algs/rs");
+
+start_stop.addStartupBatches(suite);
+
+// every time a new token is sent out, let's update the global
+// var 'token'
+var token = undefined;
+
+// create a new account via the api with (first address)
+suite.addBatch({
+  "staging an account": {
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: 'first@fakeemail.com',
+      pass: 'firstfakepass',
+      site:'http://localhost:123'
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      start_stop.waitForToken(this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
+    }
+  }
+});
+
+suite.addBatch({
+  "create first account": {
+    topic: function() {
+      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
+    },
+    "account created": function(err, r) {
+      assert.equal(r.code, 200);
+      assert.strictEqual(true, JSON.parse(r.body).success);
+      token = undefined;
+    }
+  }
+});
+
+suite.addBatch({
+  "email created": {
+    topic: wsapi.get('/wsapi/user_creation_status', { email: 'first@fakeemail.com' } ),
+    "should exist": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).status, "complete");
+    }
+  }
+});
+
+// add a new email address to the account (second address)
+suite.addBatch({
+  "add a new email address to our account": {
+    topic: wsapi.post('/wsapi/stage_email', {
+      email: 'second@fakeemail.com',
+      site:'https://fakesite.foobar.bizbaz.uk'
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      start_stop.waitForToken(this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
+    }
+  }
+});
+
+// confirm second email email address to the account
+suite.addBatch({
+  "create second account": {
+    topic: function() {
+      wsapi.post('/wsapi/complete_email_confirmation', { token: token }).call(this);
+    },
+    "account created": function(err, r) {
+      assert.equal(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).success, true);
+      token = undefined;
+    }
+  }
+});
+
+// verify now both email addresses are known
+suite.addBatch({
+  "first email exists": {
+    topic: wsapi.get('/wsapi/have_email', { email: 'first@fakeemail.com' }),
+    "should exist": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).email_known, true);
+    }
+  },
+  "second email exists": {
+    topic: wsapi.get('/wsapi/have_email', { email: 'second@fakeemail.com' }),
+    "should exist": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).email_known, true);
+    }
+  },
+  "a random email doesn't exist": {
+    topic: wsapi.get('/wsapi/have_email', { email: 'third@fakeemail.com' }),
+    "shouldn't exist": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).email_known, false);
+    }
+  }
+});
+
+suite.addBatch({
+  "reset status": {
+    topic: wsapi.get('/wsapi/password_reset_status', { email: 'first@fakeemail.com' } ),
+    "returns 'complete' before calling reset": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).status, "complete");
+    }
+  }
+});
+
+// Run the "forgot_email" flow with first address. 
+suite.addBatch({
+  "reset password on first account": {
+    topic: wsapi.post('/wsapi/stage_reset', {
+      email: 'first@fakeemail.com',
+      pass: 'secondfakepass',
+      site:'https://otherfakesite.com'
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      start_stop.waitForToken(this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
+    }
+  }
+});
+
+suite.addBatch({
+  "given a token, getting an email": {
+    topic: function() {
+      wsapi.get('/wsapi/email_for_token', { token: token }).call(this);
+    },
+    "account created": function(err, r) {
+      assert.equal(r.code, 200);
+      var body = JSON.parse(r.body);
+      assert.strictEqual(body.success, true);
+    }
+  }
+});
+
+// verify that the old email address + password combinations are still
+// valid (this is so *until* someone clicks through)
+suite.addBatch({
+  "first email works": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: 'first@fakeemail.com',
+      pass: 'firstfakepass',
+      ephemeral: false
+    }),
+    "should work": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).success, true);
+    }
+  },
+  "second email works": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: 'second@fakeemail.com',
+      pass: 'firstfakepass',
+      ephemeral: false
+    }),
+    "should work": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).success, true);
+    }
+  },
+  "reset status": {
+    topic: wsapi.get('/wsapi/password_reset_status', { email: 'first@fakeemail.com' } ),
+    "returns 'pending' after calling reset": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).status, "pending");
+    }
+  }
+});
+
+// now let's complete the re-registration of first email address
+suite.addBatch({
+  "complete password reset": {
+    topic: function() {
+      wsapi.post('/wsapi/complete_reset', { token: token }).call(this);
+    },
+    "account created": function(err, r) {
+      assert.equal(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).success, true);
+    }
+  }
+});
+
+suite.addBatch({
+  "reset status": {
+    topic: wsapi.get('/wsapi/password_reset_status', { email: 'first@fakeemail.com' } ),
+    "returns 'complete' after completing reset": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).status, "complete");
+    }
+  }
+});
+
+// now we should be able to sign in using any email address
+suite.addBatch({
+  "first email, first pass bad": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: 'first@fakeemail.com',
+      pass: 'firstfakepass',
+      ephemeral: false
+    }),
+    "shouldn't work": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).success, false);
+    }
+  },
+  "first email, second pass good": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: 'first@fakeemail.com',
+      pass: 'secondfakepass',
+      ephemeral: false
+    }),
+    "should work": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).success, true);
+    }
+  },
+  "second email, first pass bad": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: 'second@fakeemail.com',
+      pass: 'firstfakepass',
+      ephemeral: false
+    }),
+    "should work": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).success, false);
+    }
+  },
+  "second email, second pass bad": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: 'second@fakeemail.com',
+      pass: 'secondfakepass',
+      ephemeral: false
+    }),
+    "shouldn' work": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).success, true);
+    }
+  },
+});
+
+// test list emails
+suite.addBatch({
+  "list emails API": {
+    topic: wsapi.get('/wsapi/list_emails', {}),
+    "succeeds with HTTP 200" : function(err, r) {
+      assert.strictEqual(r.code, 200);
+    },
+    "returns an object with proper bits set": function(err, r) {
+      r = JSON.parse(r.body);
+      assert.strictEqual(r['second@fakeemail.com'].verified, false);
+      assert.strictEqual(r['first@fakeemail.com'].verified, true);
+    }
+  }
+});
+
+// test that certification fails for unverified email addresses
+
+// generate a keypair, we'll use this to sign assertions, as if
+// this keypair is stored in the browser localStorage
+var kp;
+
+suite.addBatch({
+  "generate a keypair": {
+    topic: function() {
+      jwcrypto.generateKeypair({algorithm: "RS", keysize: 64}, this.callback);
+    },
+    "works": function(err, keypair) {
+      assert.isNull(err);
+      assert.isObject(keypair);
+      kp = keypair;
+    },
+    "and cert a key for a verified email address": {
+      topic: function() {
+        wsapi.post('/wsapi/cert_key', {
+          email: 'first@fakeemail.com',
+          pubkey: kp.publicKey.serialize(),
+          ephemeral: false
+        }).call(this);
+      },
+      "returns a success response" : function(err, r) {
+        assert.strictEqual(r.code, 200);
+      }
+    },
+    "and cert a key for an unverified email address": {
+      topic: function() {
+        wsapi.post('/wsapi/cert_key', {
+          email: 'second@fakeemail.com',
+          pubkey: kp.publicKey.serialize(),
+          ephemeral: false
+        }).call(this);
+      },
+      "is forbidden" : function(err, r) {
+        assert.strictEqual(r.code, 403);
+      }
+    }
+  }
+});
+
+// Now we have an account with an unverified email.  Let's attempt to reverify our other email
+// address
+// Run the "forgot_email" flow with first address. 
+suite.addBatch({
+  "reverify a non-existent email": {
+    topic: wsapi.post('/wsapi/stage_reverify', {
+      email: 'dne@fakeemail.com',
+      site:'https://otherfakesite.com'
+    }),
+    "fails": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).success, false);
+    }
+  },
+  "reverify a verified email": {
+    topic: wsapi.post('/wsapi/stage_reverify', {
+      email: 'first@fakeemail.com',
+      site:'https://otherfakesite.com'
+    }),
+    "fails": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).success, false);
+    }
+  },
+  "reverify an unverified email": {
+    topic: wsapi.post('/wsapi/stage_reverify', {
+      email: 'second@fakeemail.com',
+      site:'https://otherfakesite.com'
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).success, true);
+    }
+  }
+});
+
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      start_stop.waitForToken(this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
+    }
+  }
+});
+
+suite.addBatch({
+  "given a token, getting an email": {
+    topic: function() {
+      wsapi.get('/wsapi/email_for_token', { token: token }).call(this);
+    },
+    "works dandy": function(err, r) {
+      assert.equal(r.code, 200);
+      var body = JSON.parse(r.body);
+      assert.strictEqual(body.success, true);
+      assert.strictEqual(body.email, 'second@fakeemail.com');
+      assert.strictEqual(body.must_auth, false);
+    }
+  }
+});
+
+suite.addBatch({
+  "reverify status": {
+    topic: function() {
+      wsapi.get('/wsapi/email_reverify_status', { email: "second@fakeemail.com" }).call(this);
+    },
+    "is pending": function(err, r) {
+      assert.equal(r.code, 200);
+      var body = JSON.parse(r.body);
+      assert.strictEqual(body.status, 'pending');
+    }
+  }
+});
+
+suite.addBatch({
+  "complete reverify": {
+    topic: function() {
+      wsapi.post('/wsapi/complete_email_confirmation', { token: token }).call(this);
+    },
+    "works": function(err, r) {
+      assert.equal(r.code, 200);
+      var body = JSON.parse(r.body);
+    }
+  }
+});
+
+suite.addBatch({
+  "after reverification": {
+    topic: function() {
+      jwcrypto.generateKeypair({algorithm: "RS", keysize: 64}, this.callback);
+    },
+    "we can generate a keypair": function(err, keypair) {
+      assert.isNull(err);
+      assert.isObject(keypair);
+      kp = keypair;
+    },
+    "we can certify a key for the email address": {
+      topic: function() {
+        wsapi.post('/wsapi/cert_key', {
+          email: 'second@fakeemail.com',
+          pubkey: kp.publicKey.serialize(),
+          ephemeral: false
+        }).call(this);
+      },
+      "returns a success response" : function(err, r) {
+        assert.strictEqual(r.code, 200);
+      }
+    }
+  }
+});
+
+
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/tests/list-emails-wsapi-test.js b/tests/list-emails-wsapi-test.js
index 037bed0b812225b6618a231c75f94ec25e12bf33..0eb3449b7461b485b3ddb31f1cd1e5a7898d272b 100755
--- a/tests/list-emails-wsapi-test.js
+++ b/tests/list-emails-wsapi-test.js
@@ -85,6 +85,7 @@ suite.addBatch({
       var emails = Object.keys(respObj);
       assert.equal(emails[0], "syncer@somehost.com");
       assert.equal(respObj[emails[0]].type, "secondary");
+      assert.equal(respObj[emails[0]].verified, true);
       assert.equal(emails.length, 1);
     }
   }
diff --git a/tests/page-requests-test.js b/tests/page-requests-test.js
index 57f9b081300a171366a24b1c72a4bf43405d4605..a6fd57988775c0fe5c8cfb04a0811a40fc3695d2 100755
--- a/tests/page-requests-test.js
+++ b/tests/page-requests-test.js
@@ -63,6 +63,9 @@ suite.addBatch({
   'GET /privacy':                respondsWith(200),
   'GET /verify_email_address':   respondsWith(200),
   'GET /add_email_address':      respondsWith(200),
+  'GET /confirm':                respondsWith(200),
+  'GET /reset_password':         respondsWith(200),
+  'GET /confirm':                respondsWith(200),
   'GET /idp_auth_complete':      respondsWith(200),
   'GET /pk':                     respondsWith(200),
   'GET /.well-known/browserid':  respondsWith(200),
diff --git a/tests/primary-then-secondary-test.js b/tests/primary-then-secondary-test.js
index 2112b3e2a674b6f5f16992d55996c2a30b7e36fa..1eb6aaeda15e3cc62ba4954a317ac910291b5989 100755
--- a/tests/primary-then-secondary-test.js
+++ b/tests/primary-then-secondary-test.js
@@ -122,7 +122,7 @@ suite.addBatch({
           },
           "which then": {
             topic: function() {
-              wsapi.post('/wsapi/complete_email_addition', {
+              wsapi.post('/wsapi/complete_email_confirmation', {
                 token: this._token
               }).call(this);
             },
@@ -200,7 +200,7 @@ suite.addBatch({
           },
           "with a token": {
             topic: function() {
-              wsapi.post('/wsapi/complete_email_addition', {
+              wsapi.post('/wsapi/complete_email_confirmation', {
                 token: this._token
               }).call(this);
             },
diff --git a/tests/stalled-mysql-test.js b/tests/stalled-mysql-test.js
index 5bf27ce9716093340a626e60f44b48c0bb902c5d..5337ad2950c1cc19285fec08f8547ecc73e9a548 100755
--- a/tests/stalled-mysql-test.js
+++ b/tests/stalled-mysql-test.js
@@ -119,8 +119,8 @@ suite.addBatch({
       assert.strictEqual(r.code, 503);
     }
   },
-  "complete_email_addition": {
-    topic: wsapi.post('/wsapi/complete_email_addition', {
+  "complete_email_confirmation": {
+    topic: wsapi.post('/wsapi/complete_email_confirmation', {
       token: 'bogus'
     }),
     "fails with 503": function(err, r) {
diff --git a/tests/verify-in-different-browser-test.js b/tests/verify-in-different-browser-test.js
index b5a5a0fbb5f5cf6463088a36bba301a3b29e67f1..9cd451b6c4122de385924a9c1d9b1fb847d08f87 100755
--- a/tests/verify-in-different-browser-test.js
+++ b/tests/verify-in-different-browser-test.js
@@ -100,7 +100,7 @@ suite.addBatch({
       "then clearing cookies and completing": {
         topic: function() {
           wsapi.clearCookies();
-          wsapi.post('/wsapi/complete_email_addition', {
+          wsapi.post('/wsapi/complete_email_confirmation', {
             token: this._token
           }).call(this);
         },
@@ -109,7 +109,7 @@ suite.addBatch({
         },
         "but succeeds": {
           topic: function() {
-            wsapi.post('/wsapi/complete_email_addition', {
+            wsapi.post('/wsapi/complete_email_confirmation', {
               token: this._token,
               pass: TEST_PASS
             }).call(this);
@@ -171,7 +171,7 @@ suite.addBatch({
       "then clearing cookies and completing": {
         topic: function() {
           wsapi.clearCookies();
-          wsapi.post('/wsapi/complete_email_addition', {
+          wsapi.post('/wsapi/complete_email_confirmation', {
             token: this._token
           }).call(this);
         },
@@ -180,7 +180,7 @@ suite.addBatch({
         },
         "but succeeds": {
           topic: function() {
-            wsapi.post('/wsapi/complete_email_addition', {
+            wsapi.post('/wsapi/complete_email_confirmation', {
               token: this._token,
               pass: TEST_PASS
             }).call(this);
@@ -254,7 +254,7 @@ suite.addBatch({
         },
         "but succeeds": {
           topic: function() {
-            wsapi.post('/wsapi/complete_email_addition', {
+            wsapi.post('/wsapi/complete_user_creation', {
               token: this._token,
               pass: TEST_PASS
             }).call(this);