diff --git a/bin/browserid b/bin/browserid
index 6f23273cbcb5e5c86adc231d3831e548b7175c6b..03fa955e44d9c29e754dee7b3bc5721ec32c2155 100755
--- a/bin/browserid
+++ b/bin/browserid
@@ -24,7 +24,7 @@ config = require('../lib/configuration.js'),
 heartbeat = require('../lib/heartbeat.js'),
 metrics = require('../lib/metrics.js'),
 logger = require('../lib/logging.js').logger,
-forward = require('../lib/http_forward'),
+forward = require('../lib/http_forward').forward,
 shutdown = require('../lib/shutdown'),
 views = require('../lib/browserid/views.js');
 
diff --git a/bin/proxy b/bin/proxy
index 02b2da157cdbf29799957d5d764aec5cdf2e45ce..ce26d11277480ae7ea312db34bcf5c17da061a24 100755
--- a/bin/proxy
+++ b/bin/proxy
@@ -14,6 +14,9 @@ config = require('../lib/configuration.js');
 var port = config.has('bind_to.port') ? config.get('bind_to.port') : 0;
 var addy = config.has('bind_to.host') ? config.get('bind_to.host') : "127.0.0.1";
 
+// set a maximum allowed time on responses to declaration of support requests
+forward.setTimeout(config.get('declaration_of_support_timeout_ms'));
+
 const allowed = /^https:\/\/[a-zA-Z0-9\.\-_]+\/\.well-known\/browserid$/;
 
 var server = http.createServer(function (req, res) {
@@ -24,7 +27,7 @@ var server = http.createServer(function (req, res) {
     return;
   }
 
-  forward(url, req, res, function(err) {
+  forward.forward(url, req, res, function(err) {
     if (err) {
       res.writeHead(400);
       res.end('Oops: ' + err.toString());
diff --git a/lib/configuration.js b/lib/configuration.js
index 7e6c71935737d60d2e9d3e293ee1d4a2c5f2b71c..4e576d079f346b009b903fb9b3d4ae66ea0d4959 100644
--- a/lib/configuration.js
+++ b/lib/configuration.js
@@ -193,7 +193,11 @@ var conf = module.exports = convict({
     env: 'DBWRITER_URL'
   },
   process_type: 'string',
-  email_to_console: 'boolean = false'
+  email_to_console: 'boolean = false',
+  declaration_of_support_timeout_ms: {
+    doc: "The amount of time we wait for a server to respond with a declaration of support, before concluding that they are not a primary.  Only relevant when our local proxy is in use, not in production or staging",
+    format: 'integer = 15000'
+  }
 });
 
 // At the time this file is required, we'll determine the "process name" for this proc
diff --git a/lib/db.js b/lib/db.js
index 6765c28fbc71ad132d2c3e5dd69352954c2e7b50..1f08f6e99c5d421bb53c4c4a35b5a116e452cf37 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -73,20 +73,21 @@ exports.onReady = function(f) {
 
 // these are read only database calls
 [
+  'authForVerificationSecret',
+  'checkAuth',
+  'emailForVerificationSecret',
   'emailKnown',
-  'userKnown',
-  'isStaged',
+  'emailToUID',
+  'emailType',
   'emailsBelongToSameAccount',
-  'emailForVerificationSecret',
   'haveVerificationSecret',
-  'verificationSecretForEmail',
-  'checkAuth',
-  'listEmails',
+  'isStaged',
   'lastStaged',
+  'listEmails',
   'ping',
-  'emailType',
+  'userKnown',
   'userOwnsEmail',
-  'emailToUID'
+  'verificationSecretForEmail'
 ].forEach(function(fn) {
   exports[fn] = function() {
     checkReady();
diff --git a/lib/db/json.js b/lib/db/json.js
index b9a4c64088258d22a75452e88521af511239dc28..e49615c1fb4f5c2f495c9246cb8bcb4dbb695684 100644
--- a/lib/db/json.js
+++ b/lib/db/json.js
@@ -175,13 +175,14 @@ function addEmailToAccount(userID, email, type, cb) {
   });
 }
 
-exports.stageUser = function(email, cb) {
+exports.stageUser = function(email, hash, cb) {
   secrets.generate(48, function(secret) {
     // overwrite previously staged users
     sync();
     db.staged[secret] = {
       type: "add_account",
       email: email,
+      passwd: hash,
       when: (new Date()).getTime()
     };
     db.stagedEmails[email] = secret;
@@ -190,7 +191,7 @@ exports.stageUser = function(email, cb) {
   });
 };
 
-exports.stageEmail = function(existing_user, new_email, cb) {
+exports.stageEmail = function(existing_user, new_email, hash, cb) {
   secrets.generate(48, function(secret) {
     // overwrite previously staged users
     sync();
@@ -198,6 +199,7 @@ exports.stageEmail = function(existing_user, new_email, cb) {
       type: "add_email",
       existing_user: existing_user,
       email: new_email,
+      passwd: hash,
       when: (new Date()).getTime()
     };
     db.stagedEmails[new_email] = secret;
@@ -234,15 +236,26 @@ exports.emailForVerificationSecret = function(secret, cb) {
   process.nextTick(function() {
     sync();
     if (!db.staged[secret]) return cb("no such secret");
+    cb(null, db.staged[secret].email, db.staged[secret].existing_user);
+  });
+};
+
+exports.authForVerificationSecret = function(secret, cb) {
+  process.nextTick(function() {
+    sync();
+    if (!db.staged[secret]) return cb("no such secret");
+
+    if (db.staged[secret].passwd) {
+      return cb(null, db.staged[secret].passwd, db.staged[secret].existing_user);
+    }
+
     exports.checkAuth(db.staged[secret].existing_user, function (err, hash) {
-      cb(err, {
-        email: db.staged[secret].email,
-        needs_password: !hash
-      });
+      cb(err, hash, db.staged[secret].existing_user);
     });
   });
 };
 
+
 exports.verificationSecretForEmail = function(email, cb) {
   setTimeout(function() {
     sync();
@@ -250,7 +263,7 @@ exports.verificationSecretForEmail = function(email, cb) {
   }, 0);
 };
 
-exports.gotVerificationSecret = function(secret, hash, cb) {
+exports.gotVerificationSecret = function(secret, cb) {
   sync();
   if (!db.staged.hasOwnProperty(secret)) return cb("unknown secret");
 
@@ -265,6 +278,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) {
         var emailVal = {};
         emailVal[o.email] = { type: 'secondary' };
         var uid = getNextUserID();
+        var hash = o.passwd;
         db.users.push({
           id: uid,
           password: hash,
@@ -293,7 +307,17 @@ exports.gotVerificationSecret = function(secret, hash, cb) {
     exports.emailKnown(o.email, function(err, known) {
       function addIt() {
         addEmailToAccount(o.existing_user, o.email, 'secondary', function(e) {
-          cb(e, o.email, o.existing_user);
+          var hash = o.passwd;
+          if(e || hash === null) 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) {
diff --git a/lib/db/mysql.js b/lib/db/mysql.js
index b984dcbf438fd4a6bcaf3cb2c26f0666b66b9228..5e5dcf98eb4b624678322afe74ab29a0a645bcba 100644
--- a/lib/db/mysql.js
+++ b/lib/db/mysql.js
@@ -24,6 +24,7 @@
  *    | bool new_acct          |
  *    | int existing_user      |
  *    |*string email           |
+ *    |*string passwd          |
  *    | timestamp ts           |
  *    +------------------------+
  */
@@ -78,6 +79,7 @@ const schemas = [
     "new_acct BOOL NOT NULL," +
     "existing_user BIGINT," +
     "email VARCHAR(255) UNIQUE NOT NULL," +
+    "passwd CHAR(64)," +
     "ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL," +
     "FOREIGN KEY existing_user_fkey (existing_user) REFERENCES user(id)" +
     ") ENGINE=InnoDB;",
@@ -240,12 +242,12 @@ exports.lastStaged = function(email, cb) {
   );
 };
 
-exports.stageUser = function(email, cb) {
+exports.stageUser = function(email, hash, cb) {
   secrets.generate(48, function(secret) {
     // overwrite previously staged users
-    client.query('INSERT INTO staged (secret, new_acct, email) VALUES(?,TRUE,?) ' +
+    client.query('INSERT INTO staged (secret, new_acct, email, passwd) VALUES(?,TRUE,?,?) ' +
                  'ON DUPLICATE KEY UPDATE secret=VALUES(secret), existing_user=NULL, new_acct=TRUE, ts=NOW()',
-                 [ secret, email ],
+                 [ secret, email, hash ],
                  function(err) {
                    cb(err, err ? undefined : secret);
                  });
@@ -263,7 +265,20 @@ exports.haveVerificationSecret = function(secret, cb) {
 
 exports.emailForVerificationSecret = function(secret, cb) {
   client.query(
-    "SELECT * FROM staged WHERE secret = ?", [ secret ],
+    "SELECT email, existing_user FROM staged WHERE secret = ?", [ secret ],
+    function(err, rows) {
+      if (err) return cb("database unavailable");
+
+      // if the record was not found, fail out
+      if (!rows || rows.length != 1) return cb("no such secret");
+
+      cb(null, rows[0].email, rows[0].existing_user);
+    });
+};
+
+exports.authForVerificationSecret = function(secret, cb) {
+  client.query(
+    "SELECT existing_user, passwd FROM staged WHERE secret = ?", [ secret ],
     function(err, rows) {
       if (err) return cb("database unavailable");
 
@@ -272,34 +287,15 @@ exports.emailForVerificationSecret = function(secret, cb) {
 
       var o = rows[0];
 
-      // if the record was found and this is for a new_acct, return the email
-      if (o.new_acct) return cb(null, { email: o.email, needs_password: false });
-
-      // we need a userid.  the old schema had an 'existing' field which was an email
-      // address.  the new schema has an 'existing_user' field which is a userid.
-      // this is transitional code so outstanding verification links continue working
-      // and can be removed in feb 2012 some time.  maybe for valentines day?
-      if (typeof o.existing_user === 'number') doCheckAuth(o.existing_user);
-      else if (typeof o.existing === 'string') {
-        exports.emailToUID(o.existing, function(err, uid) {
-          if (err || uid === undefined) return cb('acct associated with staged email doesn\'t exist');
-          doCheckAuth(uid);
-        });
-      }
+      // if there is a hashed passwd in the result, we're done
+      if (o.passwd) return cb(null, o.passwd, o.existing_user);
 
-      function doCheckAuth(uid) {
-        // if the account is being added to an existing account, let's find
-        // out if the account has a password set (if only primary email addresses
-        // are associated with the acct at the moment, then there will not be a
-        // password set and the user will need to set one with the addition of
-        // this addresss)
-        exports.checkAuth(uid, function(err, hash) {
-          cb(err, {
-            email: o.email,
-            needs_password: !hash
-          });
-        });
-      }
+      // otherwise, let's get the passwd from the user record
+      if (!o.existing_user) cb("no password for user");
+
+      exports.checkAuth(o.existing_user, function(err, hash) {
+        cb(err, hash, o.existing_user);
+      });
     });
 };
 
@@ -334,7 +330,7 @@ function addEmailToUser(userID, email, type, cb) {
 }
 
 
-exports.gotVerificationSecret = function(secret, hash, cb) {
+exports.gotVerificationSecret = function(secret, cb) {
   client.query(
     "SELECT * FROM staged WHERE secret = ?", [ secret ],
     function(err, rows) {
@@ -349,6 +345,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) {
         client.query("DELETE LOW_PRIORITY FROM staged WHERE secret = ?", [ secret ]);
 
         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(?)",
@@ -362,17 +359,25 @@ exports.gotVerificationSecret = function(secret, hash, cb) {
           // address.  the new schema has an 'existing_user' field which is a userid.
           // this is transitional code so outstanding verification links continue working
           // and can be removed in feb 2012 some time.  maybe for valentines day?
-          if (typeof o.existing_user === 'number') doAddEmail(o.existing_user);
+          if (typeof o.existing_user === 'number') doAddEmailSetPassword(o.existing_user);
           else if (typeof o.existing === 'string') {
             exports.emailToUID(o.existing, function(uid) {
               if (err || uid === undefined) return cb('acct associated with staged email doesn\'t exist');
-              doAddEmail(uid);
+              doAddEmailSetPassword(uid);
             });
           }
-          function doAddEmail(uid) {
+          function doAddEmailSetPassword(uid) {
             // we're adding an email address to an existing user account.  add appropriate entries into
             // email table
-            addEmailToUser(uid, o.email, 'secondary', cb);
+            var hash = o.passwd;
+            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);
+            }
           }
         };
       }
@@ -420,12 +425,12 @@ exports.userOwnsEmail = function(uid, email, cb) {
     });
 }
 
-exports.stageEmail = function(existing_user, new_email, cb) {
+exports.stageEmail = function(existing_user, new_email, hash, cb) {
   secrets.generate(48, function(secret) {
     // overwrite previously staged users
-    client.query('INSERT INTO staged (secret, new_acct, existing_user, email) VALUES(?,FALSE,?,?) ' +
+    client.query('INSERT INTO staged (secret, new_acct, existing_user, email, passwd) VALUES(?,FALSE,?,?,?) ' +
                  'ON DUPLICATE KEY UPDATE secret=VALUES(secret), existing_user=VALUES(existing_user), new_acct=FALSE, ts=NOW()',
-                 [ secret, existing_user, new_email ],
+                 [ secret, existing_user, new_email, hash ],
                  function(err) {
                    cb(err, err ? undefined : secret);
                  });
diff --git a/lib/http_forward.js b/lib/http_forward.js
index 5277aa95643a31659a9a45085902e5b31cc37065..d88cbd85de18c4f97f85fbb563fc0929dc05c30d 100644
--- a/lib/http_forward.js
+++ b/lib/http_forward.js
@@ -9,7 +9,21 @@ https = require('https'),
 logger = require('./logging.js').logger,
 querystring = require('querystring');
 
-module.exports = function(dest, req, res, cb) {
+var global_forward_timeout = undefined;
+
+exports.setTimeout = function(to) {
+  if (typeof to != 'number') throw "setTimeout expects a numeric argument";
+  global_forward_timeout = to;
+};
+
+exports.forward = function(dest, req, res, cb) {
+  var _cb = cb;
+  var requestTimeout = undefined;
+  cb = function() {
+    if (requestTimeout) clearTimeout(requestTimeout);
+    if (_cb) _cb.apply(null, arguments);
+  }
+
   function cleanupReq() {
     if (preq) {
       preq.removeAllListeners();
@@ -56,6 +70,10 @@ module.exports = function(dest, req, res, cb) {
     cb(e);
   });
 
+  if (global_forward_timeout) {
+    requestTimeout = setTimeout(function() { preq.destroy(); }, global_forward_timeout);
+  }
+
   if (req.headers['content-type']) {
     preq.setHeader('Content-Type', req.headers['content-type']);
   }
diff --git a/lib/httputils.js b/lib/httputils.js
index 81e68334d52bcf82484bef7e0d086f8ab471e17f..cbc7a8c655f5fce4a18abd4a1bf0be9f1bee903f 100644
--- a/lib/httputils.js
+++ b/lib/httputils.js
@@ -28,6 +28,10 @@ exports.serviceUnavailable = function(resp, reason) {
   sendResponse(resp, "Service Unavailable", reason, 503);
 };
 
+exports.authRequired = function(resp, reason) {
+  sendResponse(resp, "Authentication Required", reason, 401);
+};
+
 exports.badRequest = function(resp, reason) {
   sendResponse(resp, "Bad Request", reason, 400);
 };
diff --git a/lib/primary.js b/lib/primary.js
index 55861ece63a6614d02107bf4b4927705d3031e83..5e7f481d43acbc617408affb594cadad418190d4 100644
--- a/lib/primary.js
+++ b/lib/primary.js
@@ -186,7 +186,7 @@ exports.checkSupport = function(domain, cb, delegates) {
   if (typeof domain !== 'string' || !domain.length) {
     return process.nextTick(function() { cb("invalid domain"); });
   }
-  getWellKnown(domain, delegates, function (err, body, domain, delegates) {
+  getWellKnown(domain, delegates, function (err, body, domain, cbdelegates) {
     if (err) {
       logger.debug(err);
       return cb(err);
@@ -196,7 +196,7 @@ exports.checkSupport = function(domain, cb, delegates) {
       }
 
       try {
-        var r = parseWellKnownBody(body, domain, delegates, function (err, r) {
+        var r = parseWellKnownBody(body, domain, cbdelegates, function (err, r) {
           if (err) {
             logger.debug(err);
             cb(err);
@@ -226,6 +226,18 @@ exports.getPublicKey = function(domain, cb) {
   });
 };
 
+// Does emailDomain actual delegate to the issuingDomain?
+exports.delegatesAuthority = function (emailDomain, issuingDomain, cb) {
+  exports.checkSupport(emailDomain, function(err, urls, publicKey) {
+    // Check http or https://{issuingDomain}/some/sign_in_path
+    if (! err && urls && urls.auth &&
+        urls.auth.indexOf('://' + issuingDomain + '/') !== -1) {
+      cb(true);
+    }
+    cb(false);
+  });
+}
+
 // verify an assertion generated to authenticate to browserid
 exports.verifyAssertion = function(assertion, cb) {
   if (config.get('disable_primary_support')) {
diff --git a/lib/static_resources.js b/lib/static_resources.js
index 8f4d85c9947b850a366f82f39d8dc1e0a64cd5ac..7aade4d0e34c4c160c09985bd7d2470ba55a4ce7 100644
--- a/lib/static_resources.js
+++ b/lib/static_resources.js
@@ -62,8 +62,7 @@ var browserid_js = und.flatten([
     '/pages/page_helpers.js',
     '/pages/index.js',
     '/pages/start.js',
-    '/pages/add_email_address.js',
-    '/pages/verify_email_address.js',
+    '/pages/verify_secondary_address.js',
     '/pages/forgot.js',
     '/pages/manage_account.js',
     '/pages/signin.js',
@@ -91,7 +90,6 @@ var dialog_js = und.flatten([
     '/dialog/controllers/actions.js',
     '/dialog/controllers/dialog.js',
     '/dialog/controllers/authenticate.js',
-    '/dialog/controllers/forgot_password.js',
     '/dialog/controllers/check_registration.js',
     '/dialog/controllers/pick_email.js',
     '/dialog/controllers/add_email.js',
@@ -101,6 +99,7 @@ var dialog_js = und.flatten([
     '/dialog/controllers/primary_user_provisioned.js',
     '/dialog/controllers/generate_assertion.js',
     '/dialog/controllers/is_this_your_computer.js',
+    '/dialog/controllers/set_password.js',
 
     '/dialog/start.js'
   ]]);
diff --git a/lib/verifier/certassertion.js b/lib/verifier/certassertion.js
index b437fe4f9abbc5ecbcb38b66ae71e21557ceda50..babd1eae4e55096bad33b0016b243c876ff2a521 100644
--- a/lib/verifier/certassertion.js
+++ b/lib/verifier/certassertion.js
@@ -134,20 +134,30 @@ function verify(assertion, audience, successCB, errorCB) {
         return errorCB("audience mismatch: " + err);
       }
 
-      // verify that the issuer is the same as the email domain
-      // NOTE: for "delegation of authority" support we'll need to make this check
-      // more sophisticated
+      var token_verify = function (tok, pk, principal, ultimateIssuer) {
+        if (tok.verify(pk)) {
+          return successCB(principal.email, tok.audience, tok.expires, ultimateIssuer);
+        } else {
+          return errorCB("verification failure");
+        }
+      }
+
+      // verify that the issuer is the same as the email domain or
+      // that the email's domain delegated authority to the issuer
       var domainFromEmail = principal.email.replace(/^.*@/, '');
+
       if (ultimateIssuer != HOSTNAME && ultimateIssuer !== domainFromEmail)
       {
-        return errorCB("issuer issue '" + ultimateIssuer + "' may not speak for emails from '"
-                       + domainFromEmail + "'");
-      }
-
-      if (tok.verify(pk)) {
-        successCB(principal.email, tok.audience, tok.expires, ultimateIssuer);
+          primary.delegatesAuthority(domainFromEmail, ultimateIssuer, function (delegated) {
+            if (delegated) {
+              return token_verify(tok, pk, principal, ultimateIssuer);
+            } else {
+              return errorCB("issuer issue '" + ultimateIssuer + "' may not speak for emails from '"
+                         + domainFromEmail + "'");
+            }
+          });
       } else {
-        errorCB("verification failure");
+        return token_verify(tok, pk, principal, ultimateIssuer);
       }
     }, errorCB);
 };
diff --git a/lib/wsapi.js b/lib/wsapi.js
index c76c7e3e23bf501492316c40eb4dcf1b3a4c4c0a..7b736425feb5113c86a8f8d9ca91fddfe3204e87 100644
--- a/lib/wsapi.js
+++ b/lib/wsapi.js
@@ -22,7 +22,7 @@ secrets = require('./secrets'),
 config = require('./configuration'),
 logger = require('./logging.js').logger,
 httputils = require('./httputils.js'),
-forward = require('./http_forward.js'),
+forward = require('./http_forward.js').forward,
 url = require('url'),
 fs = require('fs'),
 path = require('path'),
diff --git a/lib/wsapi/address_info.js b/lib/wsapi/address_info.js
index 68284f964fd8f325adabbf53c91d3a75e4201b4b..778bd4291953da0cc9af22988a039bd80e67cab3 100644
--- a/lib/wsapi/address_info.js
+++ b/lib/wsapi/address_info.js
@@ -23,29 +23,29 @@ exports.i18n = false;
 
 const emailRegex = /\@(.*)$/;
 
-exports.process = function(req, resp) {
+exports.process = function(req, res) {
   // parse out the domain from the email
   var email = url.parse(req.url, true).query['email'];
   var m = emailRegex.exec(email);
   if (!m) {
-    return httputils.badRequest(resp, "invalid email address");
+    return httputils.badRequest(res, "invalid email address");
   }
 
-  primary.checkSupport(m[1], function(err, urls, publicKey) {
+  primary.checkSupport(m[1], function(err, urls, publicKey, delegates) {
     if (err) {
       logger.warn('error checking "' + m[1] + '" for primary support: ' + err);
-      return httputils.serverError(resp, "can't check email address");
+      return httputils.serverError(res, "can't check email address");
     }
 
     if (urls) {
       urls.type = 'primary';
-      resp.json(urls);
+      res.json(urls);
     } else {
       db.emailKnown(email, function(err, known) {
         if (err) {
-          return wsapi.databaseDown(resp, err);
+          return wsapi.databaseDown(res, err);
         } else {
-          resp.json({ type: 'secondary', known: known });
+          res.json({ type: 'secondary', known: known });
         }
       });
     }
diff --git a/lib/wsapi/cert_key.js b/lib/wsapi/cert_key.js
index 777d61223b6732ab620c66325baa9975eac891f5..0f0c81ef3f8d7336db02f0ad90a6719fcc086957 100644
--- a/lib/wsapi/cert_key.js
+++ b/lib/wsapi/cert_key.js
@@ -6,7 +6,7 @@ const
 db = require('../db.js'),
 httputils = require('../httputils'),
 logger = require('../logging.js').logger,
-forward = require('../http_forward.js'),
+forward = require('../http_forward.js').forward,
 config = require('../configuration.js'),
 urlparse = require('urlparse'),
 wsapi = require('../wsapi.js');
diff --git a/lib/wsapi/complete_email_addition.js b/lib/wsapi/complete_email_addition.js
index 6e7dd2a4df4ce2116142fe5603d848ae43f9cccf..fcff7281387b49d78fb53a05ad4b74187dd36db2 100644
--- a/lib/wsapi/complete_email_addition.js
+++ b/lib/wsapi/complete_email_addition.js
@@ -5,11 +5,11 @@
 const
 db = require('../db.js'),
 logger = require('../logging.js').logger,
-wsapi = require('../wsapi.js');
+wsapi = require('../wsapi.js'),
+brycpt = require('../bcrypt.js');
 
 exports.method = 'post';
 exports.writes_db = true;
-// XXX: see issue #290 - we want to require authentication here and update frontend code
 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)
@@ -17,63 +17,44 @@ exports.args = ['token'];
 exports.i18n = false;
 
 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
-  db.emailForVerificationSecret(req.body.token, function(err, r) {
-    if (err === 'database unavailable') {
+  // in order to complete an email addition, 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);
       return wsapi.databaseDown(res, err);
     }
 
-    if (!err && r.needs_password && !req.body.pass) {
-      err = "user must choose a password";
-    }
-    if (!err && !r.needs_password && req.body.pass) {
-      err = "a password may not be set at this time";
-    }
-    if (!err && r.needs_password) err = wsapi.checkPassword(req.body.pass);
-
-    if (err) {
-      logger.info("addition of email fails: " + err);
-      return res.json({
-        success: false,
-        reason: err
+    if (req.session.userid === initiator_uid) {
+      postAuthentication();
+    } else if (typeof req.body.pass === 'string') {
+      bcrypt.compare(req.body.pass, initiator_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 {
+          postAuthentication();
+        }
       });
+    } else {
+      return httputils.authRequired(res, "password required");
     }
 
-    // got verification secret's second paramter is a password.  That password
-    // will only be used on new account creation.  Because we know this is not
-    // a new account, we don't provide it.
-    db.gotVerificationSecret(req.body.token, "", function(e, email, uid) {
-      if (e) {
-        logger.warn("couldn't complete email verification: " + e);
-        wsapi.databaseDown(res, e);
-      } else {
-        // now do we need to set the password?
-        if (r.needs_password && req.body.pass) {
-          // requiring the client to wait until the bcrypt process is complete here
-          // exacerbates race conditions in front-end code.  We'll return success early,
-          // here, then update the password after the fact.  The worst thing that could
-          // happen is that password update could fail (due to extreme load), and the
-          // user will have to reset their password.
-          wsapi.authenticateSession(req.session, uid, 'password');
-          res.json({ success: true });
-
-          wsapi.bcryptPassword(req.body.pass, function(err, hash) {
-            if (err) {
-              logger.warn("couldn't bcrypt password during email verification: " + err);
-              return;
-            }
-            db.updatePassword(uid, hash, function(err) {
-              if (err) {
-                logger.warn("couldn't update password during email verification: " + err);
-              }
-            });
-          });
+    function postAuthentication() {
+      db.gotVerificationSecret(req.body.token, function(e, email, uid) {
+        if (e) {
+          logger.warn("couldn't complete email verification: " + e);
+          wsapi.databaseDown(res, e);
         } else {
+          wsapi.authenticateSession(req.session, uid, 'password');
           res.json({ success: true });
         }
-      }
-    });
+      });
+    };
   });
 };
diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js
index dca109d14da9a856d1ab7fc6792ec100a932a37c..e507e0f95e1396268a4f180100f00191ee469656 100644
--- a/lib/wsapi/complete_user_creation.js
+++ b/lib/wsapi/complete_user_creation.js
@@ -6,43 +6,66 @@ const
 db = require('../db.js'),
 wsapi = require('../wsapi.js'),
 httputils = require('../httputils'),
-logger = require('../logging.js').logger;
+logger = require('../logging.js').logger,
+bcrypt = require('../bcrypt');
 
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = false;
-exports.args = ['token','pass'];
+exports.args = ['token'];
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  // issue #155, valid password length is between 8 and 80 chars.
-  var err = wsapi.checkPassword(req.body.pass);
-  if (err) return httputils.badRequest(res, err);
+  // in order to complete a user creation, 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
+  //
+  // These protections guard against the case where an attacker can send out a bunch
+  // of verification emails, wait until a distracted internet user clicks on one,
+  // and then control a browserid account that they can use to prove they own
+  // the email address of the attacked.
 
-  // at the time the email verification is performed, we'll clear the pendingCreation
-  // data on the session.
-  delete req.session.pendingCreation;
-
-  // We should check to see if the verification secret is valid *before*
-  // bcrypting the password (which is expensive), to prevent a possible
-  // DoS attack.
-  db.haveVerificationSecret(req.body.token, function(err, known) {
-    if (err) return wsapi.databaseDown(res, err);
-
-    if (!known) return res.json({ success: false} );
-
-    // now bcrypt the password
-    wsapi.bcryptPassword(req.body.pass, function (err, hash) {
+  // is this the same browser?
+  if (typeof req.session.pendingCreation === 'string' &&
+      req.body.token === req.session.pendingCreation) {
+    postAuthentication();
+  }
+  // is a password provided?
+  else if (typeof req.body.pass === 'string') {
+    return db.authForVerificationSecret(req.body.token, function(err, hash) {
       if (err) {
-        if (err.indexOf('exceeded') != -1) {
+        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("server is too busy");
+          return httputils.serviceUnavailable(res, "server is too busy");
+        } else if (!success) {
+          return httputils.authRequired(res, "password mismatch");
+        } else {
+          postAuthentication();
         }
-        logger.error("can't bcrypt: " + err);
-        return res.json({ success: false });
-      }
+      });
+    });
+  } else {
+    return httputils.authRequired(res, 'Provide your password');
+  }
+
+  function postAuthentication() {
+    // the time the email verification is performed, we'll clear the pendingCreation
+    // data on the session.
+    delete req.session.pendingCreation;
+
+    db.haveVerificationSecret(req.body.token, function(err, known) {
+      if (err) return wsapi.databaseDown(res, err);
+
+      if (!known) return res.json({ success: false} );
 
-      db.gotVerificationSecret(req.body.token, hash, function(err, email, uid) {
+      db.gotVerificationSecret(req.body.token, function(err, email, uid) {
         if (err) {
           logger.warn("couldn't complete email verification: " + err);
           wsapi.databaseDown(res, err);
@@ -56,5 +79,5 @@ exports.process = function(req, res) {
         }
       });
     });
-  });
+  }
 };
diff --git a/lib/wsapi/email_for_token.js b/lib/wsapi/email_for_token.js
index bfb122a747e8a514d1aca045fb3f14d7567fc6de..f492bcff595e0978c7f96e24f31b287fdceb8851 100644
--- a/lib/wsapi/email_for_token.js
+++ b/lib/wsapi/email_for_token.js
@@ -19,7 +19,7 @@ exports.args = ['token'];
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  db.emailForVerificationSecret(req.query.token, function(err, r) {
+  db.emailForVerificationSecret(req.query.token, function(err, email, uid) {
     if (err) {
       if (err === 'database unavailable') {
         httputils.serviceUnavailable(res, err);
@@ -30,10 +30,23 @@ exports.process = function(req, res) {
         });
       }
     } else {
+      // must the user authenticate?  This is true if they are not authenticated
+      // as the uid who initiated the verification, and they are not on the same
+      // browser as the initiator
+      var must_auth = true;
+
+      if (uid && req.session.userid === uid) {
+        must_auth = false;
+      }
+      else if (!uid && typeof req.session.pendingCreation === 'string' &&
+               req.query.token === req.session.pendingCreation) {
+        must_auth = false;
+      }
+
       res.json({
         success: true,
-        email: r.email,
-        needs_password: r.needs_password
+        email: email,
+        must_auth: must_auth
       });
     }
   });
diff --git a/lib/wsapi/have_email.js b/lib/wsapi/have_email.js
index ec832546bc261fa1970197a5eda4c0e7312afcd0..5caf1feffbafb6ba86ea17f40af4bd1d80435d89 100644
--- a/lib/wsapi/have_email.js
+++ b/lib/wsapi/have_email.js
@@ -14,10 +14,10 @@ exports.authed = false;
 exports.args = ['email'];
 exports.i18n = false;
 
-exports.process = function(req, resp) {
+exports.process = function(req, res) {
   var email = url.parse(req.url, true).query['email'];
   db.emailKnown(email, function(err, known) {
-    if (err) return wsapi.databaseDown(resp, err);
-    resp.json({ email_known: known });
+    if (err) return wsapi.databaseDown(res, err);
+    res.json({ email_known: known });
   });
 };
diff --git a/lib/wsapi/list_emails.js b/lib/wsapi/list_emails.js
index 6da607007c3b967b53ccc4b6d77834c705db4387..dc615dff7c06fc0af6ccc4e233c7153896f0d003 100644
--- a/lib/wsapi/list_emails.js
+++ b/lib/wsapi/list_emails.js
@@ -19,10 +19,10 @@ exports.writes_db = false;
 exports.authed = 'assertion';
 exports.i18n = false;
 
-exports.process = function(req, resp) {
+exports.process = function(req, res) {
   logger.debug('listing emails for user ' + req.session.userid);
   db.listEmails(req.session.userid, function(err, emails) {
-    if (err) wsapi.databaseDown(resp, err);
-    else resp.json(emails);
+    if (err) wsapi.databaseDown(res, err);
+    else res.json(emails);
   });
 };
diff --git a/lib/wsapi/stage_email.js b/lib/wsapi/stage_email.js
index 6ffe560eefd98102908f86946076a0643f99d083..60129ddcc72117f1c9801b262d29c5dde5dc7449 100644
--- a/lib/wsapi/stage_email.js
+++ b/lib/wsapi/stage_email.js
@@ -23,6 +23,10 @@ exports.args = ['email','site'];
 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();
@@ -30,7 +34,7 @@ exports.process = function(req, res) {
   } catch(e) {
     var msg = "invalid arguments: " + e;
     logger.warn("bad request received: " + msg);
-    return httputils.badRequest(resp, msg);
+    return httputils.badRequest(res, msg);
   }
 
   db.lastStaged(req.body.email, function (err, last) {
@@ -42,23 +46,58 @@ exports.process = function(req, res) {
       return httputils.throttled(res, "Too many emails sent to that address, try again later.");
     }
 
-    try {
-      // on failure stageEmail may throw
-      db.stageEmail(req.session.userid, req.body.email, function(err, secret) {
-        if (err) return wsapi.databaseDown(res, err);
+    db.checkAuth(req.session.userid, function(err, hash) {
+      var needs_password = !hash;
 
-        var langContext = wsapi.langContext(req);
+      if (!err && needs_password && !req.body.pass) {
+        err = "user must choose a password";
+      }
+      if (!err && !needs_password && req.body.pass) {
+        err = "a password may not be set at this time";
+      }
+      if (!err && needs_password) err = wsapi.checkPassword(req.body.pass);
 
-        // store the email being added in session data
-        req.session.pendingAddition = secret;
+      if (err) {
+        logger.info("stage of email fails: " + err);
+        return res.json({
+          success: false,
+          reason: err
+        });
+      }
 
-        res.json({ success: true });
-        // let's now kick out a verification email!
-        email.sendAddAddressEmail(req.body.email, req.body.site, secret, langContext);
-      });
-    } catch(e) {
-      // we should differentiate tween' 400 and 500 here.
-      httputils.badRequest(res, e.toString());
-    }
+      if (needs_password) {
+        wsapi.bcryptPassword(req.body.pass, function(err, hash) {
+          if (err) {
+            logger.warn("couldn't bcrypt password during email verification: " + err);
+            return res.json({ success: false });
+          }
+          completeStage(hash);
+        });
+      }
+      else {
+        completeStage(null);
+      }
+
+      function completeStage(hash) {
+        try {
+          // on failure stageEmail may throw
+          db.stageEmail(req.session.userid, 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.pendingAddition = secret;
+
+            res.json({ success: true });
+            // let's now kick out a verification email!
+            email.sendAddAddressEmail(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_user.js b/lib/wsapi/stage_user.js
index 0408b7e76a53de41a380254feed56c9ae8282773..ff1dd24bf03c10cee69671ba8ef0b80786c56db3 100644
--- a/lib/wsapi/stage_user.js
+++ b/lib/wsapi/stage_user.js
@@ -19,56 +19,75 @@ sanitize = require('../sanitize');
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = false;
-exports.args = ['email','site'];
+exports.args = ['email','pass','site'];
 exports.i18n = true;
 
-exports.process = function(req, resp) {
+exports.process = function(req, res) {
   var langContext = wsapi.langContext(req);
 
-  // staging a user logs you out.
-  wsapi.clearAuthenticatedUser(req.session);
-
   // validate
   try {
     sanitize(req.body.email).isEmail();
     sanitize(req.body.site).isOrigin();
+    if(!req.body.pass) throw "missing pass";
   } catch(e) {
     var msg = "invalid arguments: " + e;
     logger.warn("bad request received: " + msg);
-    return httputils.badRequest(resp, 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(resp, err);
+    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(resp, "Too many emails sent to that address, try again later.");
+      return httputils.throttled(res, "Too many emails sent to that address, try again later.");
     }
 
-    try {
-      // upon success, stage_user returns a secret (that'll get baked into a url
-      // and given to the user), on failure it throws
-      db.stageUser(req.body.email, function(err, secret) {
-        if (err) return wsapi.databaseDown(resp, err);
+    // staging a user logs you out.
+    wsapi.clearAuthenticatedUser(req.session);
 
-        // store the email being registered in the session data
-        if (!req.session) 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("server is too busy");
+        }
+        logger.error("can't bcrypt: " + err);
+        return res.json({ success: false });
+      }
 
-        // store the secret we're sending via email in the users session, as checking
-        // that it still exists in the database is the surest way to determine the
-        // status of the email verification.
-        req.session.pendingCreation = secret;
+      try {
+        // upon success, stage_user returns a secret (that'll get baked into a url
+        // and given to the user), on failure it throws
+        db.stageUser(req.body.email, hash, function(err, secret) {
+          if (err) return wsapi.databaseDown(res, err);
 
-        resp.json({ success: true });
+          // store the email being registered in the session data
+          if (!req.session) req.session = {};
 
-        // let's now kick out a verification email!
-        email.sendNewUserEmail(req.body.email, req.body.site, secret, langContext);
-      });
-    } catch(e) {
-      // we should differentiate tween' 400 and 500 here.
-      httputils.badRequest(resp, e.toString());
-    }
+          // store the secret we're sending via email in the users session, as checking
+          // that it still exists in the database is the surest way to determine the
+          // status of the email verification.
+          req.session.pendingCreation = secret;
+
+          res.json({ success: true });
+
+          // let's now kick out a verification email!
+          email.sendNewUserEmail(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/css/style.css b/resources/static/css/style.css
index 00b561a12fae1d6333e302dbd391c089012d09ed..b41a984313fe3241ae9b4deff28fc96219ea7932 100644
--- a/resources/static/css/style.css
+++ b/resources/static/css/style.css
@@ -667,7 +667,7 @@ h1 {
   margin-bottom: 10px;
 }
 
-.siteinfo, #congrats, #signUpForm > .password_entry, .enter_password .hint, #unknown_secondary, #primary_verify, .verify_primary .submit {
+.siteinfo, #congrats, .password_entry, .enter_password .hint, #unknown_secondary, #primary_verify, .verify_primary .submit {
   display: none;
 }
 
@@ -675,7 +675,7 @@ h1 {
   float: left;
 }
 
-.enter_password #signUpForm > .password_entry, .known_secondary #signUpForm > .password_entry,
+.enter_password .password_entry, .known_secondary .password_entry,
 .unknown_secondary #unknown_secondary, .verify_primary #verify_primary {
   display: block;
 }
@@ -820,3 +820,4 @@ footer a:hover {
 .newsbanner a:hover {
   color: #000;
 }
+
diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js
index 08239b6befc50e5b56547823020dd07a5b7ef5a1..322749710534ba216c7ef8fd509c647e84260256 100644
--- a/resources/static/dialog/controllers/actions.js
+++ b/resources/static/dialog/controllers/actions.js
@@ -31,12 +31,13 @@ BrowserID.Modules.Actions = (function() {
     return module;
   }
 
-  function startRegCheckService(options, verifier, message) {
+  function startRegCheckService(options, verifier, message, password) {
     var controller = startService("check_registration", {
       email: options.email,
       required: options.required,
       verifier: verifier,
-      verificationMessage: message
+      verificationMessage: message,
+      password: password
     });
     controller.startCheck();
   }
@@ -73,8 +74,16 @@ BrowserID.Modules.Actions = (function() {
       if(onsuccess) onsuccess(null);
     },
 
+    doSetPassword: function(info) {
+      startService("set_password", info);
+    },
+
+    doStageUser: function(info) {
+      dialogHelpers.createUser.call(this, info.email, info.password, info.ready);
+    },
+
     doConfirmUser: function(info) {
-      startRegCheckService.call(this, info, "waitForUserValidation", "user_confirmed");
+      startRegCheckService.call(this, info, "waitForUserValidation", "user_confirmed", info.password || undefined);
     },
 
     doPickEmail: function(info) {
@@ -85,6 +94,10 @@ BrowserID.Modules.Actions = (function() {
       startService("add_email", info);
     },
 
+    doStageEmail: function(info) {
+      dialogHelpers.addSecondaryEmailWithPassword.call(this, info.email, info.password, info.ready);
+    },
+
     doAuthenticate: function(info) {
       startService("authenticate", info);
     },
@@ -94,11 +107,11 @@ BrowserID.Modules.Actions = (function() {
     },
 
     doForgotPassword: function(info) {
-      startService("forgot_password", info);
+      startService("set_password", _.extend(info, { password_reset: true }));
     },
 
     doResetPassword: function(info) {
-      this.doConfirmUser(info);
+      dialogHelpers.resetPassword.call(this, info.email, info.password, info.ready);
     },
 
     doConfirmEmail: function(info) {
diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js
index a842fecc0bc196695f8f690c90bc65903c387676..d81fc9db32d86699b65ac10569baa17a3d070321 100644
--- a/resources/static/dialog/controllers/authenticate.js
+++ b/resources/static/dialog/controllers/authenticate.js
@@ -18,7 +18,7 @@ BrowserID.Modules.Authenticate = (function() {
       dom = bid.DOM,
       lastEmail = "",
       addressInfo,
-      hints = ["newuser","returning","start","addressInfo"];
+      hints = ["returning","start","addressInfo"];
 
   function getEmail() {
     return helpers.getAndValidateEmail("#email");
@@ -61,7 +61,7 @@ BrowserID.Modules.Authenticate = (function() {
       else if(info.known) {
         enterPasswordState.call(self);
       } else {
-        createSecondaryUserState.call(self);
+        createSecondaryUser.call(self);
       }
     }
   }
@@ -71,9 +71,9 @@ BrowserID.Modules.Authenticate = (function() {
         email = getEmail();
 
     if (email) {
-      dialogHelpers.createUser.call(self, email, callback);
+      self.close("new_user", { email: email }, { email: email });
     } else {
-      callback && callback();
+      complete(callback);
     }
   }
 
@@ -129,15 +129,6 @@ BrowserID.Modules.Authenticate = (function() {
     }
   }
 
-  function createSecondaryUserState() {
-    var self=this;
-
-    self.publish("create_user");
-    self.submit = createSecondaryUser;
-    showHint("newuser");
-  }
-
-
   function emailKeyUp() {
     var newEmail = dom.getInner("#email");
     if (newEmail !== lastEmail) {
@@ -161,7 +152,7 @@ BrowserID.Modules.Authenticate = (function() {
         tos_url: options.tosURL
       });
 
-      $(".newuser,.returning,.start").hide();
+      $(".returning,.start").hide();
 
       self.bind("#email", "keyup", emailKeyUp);
       self.click("#forgotPassword", forgotPassword);
diff --git a/resources/static/dialog/controllers/check_registration.js b/resources/static/dialog/controllers/check_registration.js
index 3389f3f9e0ca7b47bb22e796e3c8135c91af9ce0..efd38e246b6c7ee4c19515f46c5720679949a6c9 100644
--- a/resources/static/dialog/controllers/check_registration.js
+++ b/resources/static/dialog/controllers/check_registration.js
@@ -23,6 +23,7 @@ BrowserID.Modules.CheckRegistration = (function() {
       self.verifier = options.verifier;
       self.verificationMessage = options.verificationMessage;
       self.required = options.required;
+      self.password = options.password;
 
       self.click("#back", self.back);
 
@@ -40,9 +41,26 @@ BrowserID.Modules.CheckRegistration = (function() {
           });
         }
         else if (status === "mustAuth") {
-          user.addressInfo(self.email, function(info) {
-            self.close("authenticate", info);
-          });
+          // if we have a password (because it was just chosen in dialog),
+          // then we can authenticate the user and proceed
+          if (self.password) {
+            user.authenticate(self.email, self.password, function (authenticated) {
+              if (authenticated) {
+                user.syncEmails(function() {
+                  self.close(self.verificationMessage);
+                  oncomplete && oncomplete();
+                });
+              } else {
+                user.addressInfo(self.email, function(info) {
+                  self.close("authenticate", info);
+                });
+              }
+            });
+          } else {
+            user.addressInfo(self.email, function(info) {
+              self.close("authenticate", info);
+            });
+          }
 
           oncomplete && oncomplete();
         }
diff --git a/resources/static/dialog/controllers/forgot_password.js b/resources/static/dialog/controllers/forgot_password.js
deleted file mode 100644
index 268f72417b625a66a07ade614a8a1b8d26ceb8e2..0000000000000000000000000000000000000000
--- a/resources/static/dialog/controllers/forgot_password.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
-/*global BrowserID:true, PageController: true */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-BrowserID.Modules.ForgotPassword = (function() {
-  "use strict";
-
-  var ANIMATION_TIME = 250,
-      bid = BrowserID,
-      helpers = bid.Helpers,
-      dialogHelpers = helpers.Dialog,
-      dom = bid.DOM;
-
-  function resetPassword() {
-    var self=this;
-    dialogHelpers.resetPassword.call(self, self.email);
-  }
-
-  function cancelResetPassword() {
-    this.close("cancel_state", { email: this.email });
-  }
-
-  var Module = bid.Modules.PageModule.extend({
-    start: function(options) {
-      var self=this;
-      self.email = options.email;
-      self.renderDialog("forgot_password", {
-        email: options.email || "",
-        requiredEmail: options.requiredEmail
-      });
-
-      self.click("#cancel", cancelResetPassword);
-
-      Module.sc.start.call(self, options);
-    },
-
-    submit: resetPassword
-
-    // BEGIN TESTING API
-    ,
-    resetPassword: resetPassword,
-    cancelResetPassword: cancelResetPassword
-    // END TESTING API
-  });
-
-  return Module;
-
-}());
diff --git a/resources/static/dialog/controllers/pick_email.js b/resources/static/dialog/controllers/pick_email.js
index b57933026038d0c3e1c9508d579016fda9d53538..61e65c469480a0f29a03415f979895d3506b5abf 100644
--- a/resources/static/dialog/controllers/pick_email.js
+++ b/resources/static/dialog/controllers/pick_email.js
@@ -27,14 +27,14 @@ BrowserID.Modules.PickEmail = (function() {
   }
 
   function addEmail() {
-    this.close("add_email");
+    this.publish("add_email");
   }
 
   function checkEmail(email) {
     var identity = user.getStoredEmailKeypair(email);
     if (!identity) {
       alert(gettext("The selected email is invalid or has been deleted."));
-      this.close("assertion_generated", {
+      this.publish("assertion_generated", {
         assertion: null
       });
     }
@@ -76,6 +76,10 @@ BrowserID.Modules.PickEmail = (function() {
     }
   }
 
+  function notMe() {
+    this.publish("notme");
+  }
+
   var Module = bid.Modules.PageModule.extend({
     start: function(options) {
       var origin = user.getOrigin(),
@@ -104,6 +108,7 @@ BrowserID.Modules.PickEmail = (function() {
       // is needed for the label handler so that the correct radio button is
       // selected.
       self.bind("#selectEmail label", "click", proxyEventToInput);
+      self.click("#thisIsNotMe", notMe);
 
       sc.start.call(self, options);
 
@@ -118,7 +123,8 @@ BrowserID.Modules.PickEmail = (function() {
     // BEGIN TESTING API
     ,
     signIn: signIn,
-    addEmail: addEmail
+    addEmail: addEmail,
+    notMe: notMe
     // END TESTING API
   });
 
diff --git a/resources/static/dialog/controllers/set_password.js b/resources/static/dialog/controllers/set_password.js
new file mode 100644
index 0000000000000000000000000000000000000000..308cee3288d858b05a047fbe39697d4ddc766049
--- /dev/null
+++ b/resources/static/dialog/controllers/set_password.js
@@ -0,0 +1,53 @@
+/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*global _: true, BrowserID: true, PageController: true */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+BrowserID.Modules.SetPassword = (function() {
+  "use strict";
+  var bid = BrowserID,
+      dom = bid.DOM,
+      helpers = bid.Helpers,
+      complete = helpers.complete,
+      dialogHelpers = helpers.Dialog,
+      sc;
+
+  function submit(callback) {
+    var pass = dom.getInner("#password"),
+        vpass = dom.getInner("#vpassword"),
+        options = this.options;
+
+    var valid = bid.Validation.passwordAndValidationPassword(pass, vpass);
+    if(valid) {
+      this.publish("password_set", { password: pass });
+    }
+
+    complete(callback, valid);
+  }
+
+  function cancel() {
+    this.close("cancel_state");
+  }
+
+  var Module = bid.Modules.PageModule.extend({
+    start: function(options) {
+      var self=this;
+      options = options || {};
+
+      self.renderDialog("set_password", {
+        password_reset: !!options.password_reset
+      });
+
+      self.click("#cancel", cancel);
+
+      sc.start.call(self, options);
+    },
+
+    submit: submit,
+    cancel: cancel
+  });
+
+  sc = Module.sc;
+
+  return Module;
+}());
diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js
index 12029173c581474065a9f0cf4a895003673930cf..e95f7388625b933ccd340778e6c4fcbcd416d709 100644
--- a/resources/static/dialog/resources/helpers.js
+++ b/resources/static/dialog/resources/helpers.js
@@ -39,7 +39,7 @@
     user.getAssertion(email, user.getOrigin(), function(assert) {
       assert = assert || null;
       wait.hide();
-      self.close("assertion_generated", {
+      self.publish("assertion_generated", {
         assertion: assert
       });
 
@@ -58,28 +58,28 @@
       }, self.getErrorDialog(errors.authenticate, callback));
   }
 
-  function createUser(email, callback) {
+  function createUser(email, password, callback) {
     var self=this;
-    user.createSecondaryUser(email, function(status) {
+    user.createSecondaryUser(email, password, function(status) {
       if (status) {
-        var info = { email: email };
-        self.close("user_staged", info, info);
+        var info = { email: email, password: password };
+        self.publish("user_staged", info, info);
         complete(callback, true);
       }
       else {
+        // XXX will this tooltip ever be shown, the authentication screen has
+        // already been torn down by this point?
         tooltip.showTooltip("#could_not_add");
         complete(callback, false);
       }
     }, self.getErrorDialog(errors.createUser, callback));
   }
 
-  function resetPassword(email, callback) {
+  function resetPassword(email, password, callback) {
     var self=this;
-    user.requestPasswordReset(email, function(status) {
+    user.requestPasswordReset(email, password, function(status) {
       if (status.success) {
-        self.close("reset_password", {
-          email: email
-        });
+        self.publish("password_reset", { email: email });
       }
       else {
         tooltip.showTooltip("#could_not_add");
@@ -100,25 +100,32 @@
       user.addressInfo(email, function(info) {
         if (info.type === "primary") {
           var info = _.extend(info, { email: email, add: true });
-          self.close("primary_user", info, info);
+          self.publish("primary_user", info, info);
           complete(callback, true);
         }
         else {
-          user.addEmail(email, function(added) {
-            if (added) {
-              var info = { email: email };
-              self.close("email_staged", info, info );
-            }
-            else {
-              tooltip.showTooltip("#could_not_add");
-            }
-            complete(callback, added);
-          }, self.getErrorDialog(errors.addEmail, callback));
+          self.publish("add_email_submit_with_secondary", { email: email });
+          complete(callback, true);
         }
       }, self.getErrorDialog(errors.addressInfo, callback));
     }
   }
 
+  function addSecondaryEmailWithPassword(email, password, callback) {
+    var self=this;
+
+    user.addEmail(email, password, function(added) {
+      if (added) {
+        var info = { email: email };
+        self.publish("email_staged", info, info );
+      }
+      else {
+        tooltip.showTooltip("#could_not_add");
+      }
+      complete(callback, added);
+    }, self.getErrorDialog(errors.addEmail, callback));
+  }
+
   helpers.Dialog = helpers.Dialog || {};
 
   helpers.extend(helpers.Dialog, {
@@ -126,6 +133,7 @@
     authenticateUser: authenticateUser,
     createUser: createUser,
     addEmail: addEmail,
+    addSecondaryEmailWithPassword: addSecondaryEmailWithPassword,
     resetPassword: resetPassword,
     cancelEvent: helpers.cancelEvent,
     animateClose: animateClose
diff --git a/resources/static/dialog/resources/state.js b/resources/static/dialog/resources/state.js
index a96d5b8881da56ca8efda4538b355de817b3cf7b..288dd0e0864e3a5668cb994c21ad197b31154542 100644
--- a/resources/static/dialog/resources/state.js
+++ b/resources/static/dialog/resources/state.js
@@ -20,7 +20,13 @@ BrowserID.State = (function() {
 
   function startStateMachine() {
     var self = this,
-        handleState = self.subscribe.bind(self),
+        handleState = function(msg, callback) {
+          self.subscribe(msg, function(msg, info) {
+            // This level of indirection is to ensure an info object is
+            // always present in the handler.
+            callback(msg, info || {});
+          });
+        },
         redirectToState = mediator.publish.bind(mediator),
         startAction = function(save, msg, options) {
           if (typeof save !== "boolean") {
@@ -35,8 +41,6 @@ BrowserID.State = (function() {
         cancelState = self.popState.bind(self);
 
     handleState("start", function(msg, info) {
-      info = info || {};
-
       self.hostname = info.hostname;
       self.privacyURL = info.privacyURL;
       self.tosURL = info.tosURL;
@@ -87,12 +91,39 @@ BrowserID.State = (function() {
     });
 
     handleState("authenticate", function(msg, info) {
-      info = info || {};
       info.privacyURL = self.privacyURL;
       info.tosURL = self.tosURL;
       startAction("doAuthenticate", info);
     });
 
+    handleState("new_user", function(msg, info) {
+      self.newUserEmail = info.email;
+      startAction(false, "doSetPassword", info);
+    });
+
+    handleState("password_set", function(msg, info) {
+      /* A password can be set for one of three reasons - 1) This is a new user
+       * or 2) a user is adding the first secondary address to an account that
+       * consists only of primary addresses, or 3) an existing user has
+       * forgotten their password and wants to reset it.  #1 is taken care of
+       * by newUserEmail, #2 by addEmailEmail, #3 by resetPasswordEmail.
+       */
+      info = _.extend({ email: self.newUserEmail || self.addEmailEmail || self.resetPasswordEmail }, info);
+
+      if(self.newUserEmail) {
+        self.newUserEmail = null;
+        startAction(false, "doStageUser", info);
+      }
+      else if(self.addEmailEmail) {
+        self.addEmailEmail = null;
+        startAction(false, "doStageEmail", info);
+      }
+      else if(self.resetPasswordEmail) {
+        self.resetPasswordEmail = null;
+        startAction(false, "doResetPassword", info);
+      }
+    });
+
     handleState("user_staged", function(msg, info) {
       self.stagedEmail = info.email;
       info.required = !!requiredEmail;
@@ -121,7 +152,6 @@ BrowserID.State = (function() {
     });
 
     handleState("primary_user_provisioned", function(msg, info) {
-      info = info || {};
       info.add = !!addPrimaryUser;
       // The user is is authenticated with their IdP. Two possibilities exist
       // for the email - 1) create a new account or 2) add address to the
@@ -131,7 +161,7 @@ BrowserID.State = (function() {
     });
 
     handleState("primary_user_unauthenticated", function(msg, info) {
-      info = helpers.extend(info || {}, {
+      info = helpers.extend(info, {
         add: !!addPrimaryUser,
         email: email,
         requiredEmail: !!requiredEmail,
@@ -179,8 +209,6 @@ BrowserID.State = (function() {
     });
 
     handleState("email_chosen", function(msg, info) {
-      info = info || {};
-
       var email = info.email,
           idInfo = storage.getEmail(email);
 
@@ -271,6 +299,25 @@ BrowserID.State = (function() {
       startAction("doGenerateAssertion", info);
     });
 
+    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.
+      self.resetPasswordEmail = info.email;
+      startAction(false, "doForgotPassword", 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("assertion_generated", function(msg, info) {
       self.success = true;
       if (info.assertion !== null) {
@@ -295,26 +342,8 @@ BrowserID.State = (function() {
       redirectToState("email_chosen", info);
     });
 
-    handleState("forgot_password", function(msg, info) {
-      // forgot password initiates the forgotten password flow.
-      startAction(false, "doForgotPassword", 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) {
-      info = helpers.extend(info || {}, {
+      info = helpers.extend(info, {
         privacyURL: self.privacyURL,
         tosURL: self.tosURL
       });
@@ -322,6 +351,18 @@ BrowserID.State = (function() {
       startAction("doAddEmail", info);
     });
 
+    handleState("add_email_submit_with_secondary", function(msg, info) {
+      user.passwordNeededToAddSecondaryEmail(function(passwordNeeded) {
+        if(passwordNeeded) {
+          self.addEmailEmail = info.email;
+          startAction(false, "doSetPassword", info);
+        }
+        else {
+          startAction(false, "doStageEmail", info);
+        }
+      });
+    });
+
     handleState("email_staged", function(msg, info) {
       self.stagedEmail = info.email;
       info.required = !!requiredEmail;
@@ -329,7 +370,7 @@ BrowserID.State = (function() {
     });
 
     handleState("email_confirmed", function() {
-      redirectToState("email_chosen", { email: self.stagedEmail} );
+      redirectToState("email_chosen", { email: self.stagedEmail } );
     });
 
     handleState("cancel_state", function(msg, info) {
diff --git a/resources/static/dialog/start.js b/resources/static/dialog/start.js
index 1d0655877c632dfd28b18d2fe025877c464dca4f..e0e64b71daab4c1387490ac21dcdab39d9e5690e 100644
--- a/resources/static/dialog/start.js
+++ b/resources/static/dialog/start.js
@@ -29,7 +29,6 @@
       moduleManager.register("add_email", modules.AddEmail);
       moduleManager.register("authenticate", modules.Authenticate);
       moduleManager.register("check_registration", modules.CheckRegistration);
-      moduleManager.register("forgot_password", modules.ForgotPassword);
       moduleManager.register("is_this_your_computer", modules.IsThisYourComputer);
       moduleManager.register("pick_email", modules.PickEmail);
       moduleManager.register("required_email", modules.RequiredEmail);
@@ -39,6 +38,7 @@
       moduleManager.register("generate_assertion", modules.GenerateAssertion);
       moduleManager.register("xhr_delay", modules.XHRDelay);
       moduleManager.register("xhr_disable_form", modules.XHRDisableForm);
+      moduleManager.register("set_password", modules.SetPassword);
 
       moduleManager.start("xhr_delay");
       moduleManager.start("xhr_disable_form");
diff --git a/resources/static/dialog/views/authenticate.ejs b/resources/static/dialog/views/authenticate.ejs
index 4db5074ac61beab2efad4ee8b0e1190efe6ac3b5..2e72d63e08feaf153df142917f6e96c361f43b91 100644
--- a/resources/static/dialog/views/authenticate.ejs
+++ b/resources/static/dialog/views/authenticate.ejs
@@ -35,11 +35,6 @@
               <%= gettext("Please hold on while we get information about your email provider.") %>
           </li>
 
-          <li id="create_text_section" class="newuser">
-              <strong><%= gettext('Welcome to BrowserID!') %></strong>
-              <p><%= gettext("This email looks new, so let's get you set up.") %></p>
-          </li>
-
           <li class="returning">
 
               <a id="forgotPassword" class="forgot right" href="#" tabindex="4"><%= gettext('forgot your password?') %></a>
@@ -71,7 +66,6 @@
           <p>
       <% } %>
             <button class="start addressInfo" tabindex="3"><%= gettext('next') %></button>
-            <button class="newuser" tabindex="3"><%= gettext('verify email') %></button>
             <button class="returning" tabindex="3"><%= gettext('sign in') %></button>
         <% if (privacy_url && tos_url) { %>
           </p>
diff --git a/resources/static/dialog/views/forgot_password.ejs b/resources/static/dialog/views/forgot_password.ejs
deleted file mode 100644
index 3bbd7cd8ef495f38bd4fff59075961c97b59a5db..0000000000000000000000000000000000000000
--- a/resources/static/dialog/views/forgot_password.ejs
+++ /dev/null
@@ -1,30 +0,0 @@
-<!-- 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/. -->
-
-  <strong>
-  <% if (requiredEmail) { %>
-    <%= gettext('The site requested you sign in using') %>
-  <% } else { %>
-    <%= gettext('Sign in using') %>
-  <% } %>
-  </strong>
-
-  <div class="form_section">
-      <ul class="inputs">
-          <li>
-              <label for="email" class="serif"><%= gettext('Email') %></label>
-              <input id="email" class="sans" type="email" value="<%= email %>" disabled />
-
-              <div id="could_not_add" class="tooltip" for="email">
-                <%= gettext('We just sent an email to that address! If you really want to send another, wait a minute or two and try again.') %>
-              </div>
-          </li>
-
-      </ul>
-
-      <div class="submit cf">
-          <button tabindex="1"><%= gettext('reset password') %></button>
-          <a href="#" id="cancel" class="action" tabindex="2"><%= gettext('cancel') %></a>
-      </div>
-  </div>
diff --git a/resources/static/dialog/views/set_password.ejs b/resources/static/dialog/views/set_password.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..a07b5d8737d5414e58b677313632c4b77cfc1e40
--- /dev/null
+++ b/resources/static/dialog/views/set_password.ejs
@@ -0,0 +1,54 @@
+<!-- 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/. -->
+
+  <strong><%= gettext('Welcome to BrowserID!') %></strong>
+
+  <div class="form_section" id="set_password">
+      <ul class="inputs">
+          <% if(!password_reset) { %>
+            <li>
+                <%= gettext("Next, choose a new password you'll use when you sign in with BrowserID.") %>
+            </li>
+          <% } %>
+
+
+          <li>
+              <label for="password" class="serif"><%= gettext('Password') %></label>
+              <input id="password" class="sans" type="password" maxlength="80" />
+
+              <div class="tooltip" id="password_required" for="password">
+                <%= gettext('Password is required.') %>
+              </div>
+
+              <div class="tooltip" id="password_length" for="password">
+                <%= gettext('Password must be between 8 and 80 characters long.') %>
+              </div>
+
+              <div id="could_not_add" class="tooltip" for="password">
+                <%= gettext('We just sent an email to that address! If you really want to send another, wait a minute or two and try again.') %>
+              </div>
+          </li>
+
+          <li>
+              <label class="serif" for="vpassword"><%= gettext('Verify Password') %></label>
+              <input class="sans" id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength=80 />
+
+              <div class="tooltip" id="vpassword_required" for="vpassword">
+                <%= gettext('Verification password is required.') %>
+              </div>
+
+              <div class="tooltip" id="passwords_no_match" for="vpassword">
+                <%= gettext('Passwords do not match.') %>
+              </div>
+          </li>
+
+      </ul>
+
+      <div class="submit cf">
+          <button tabindex="1" id="<%= password_reset ? "password_reset" : "verify_user" %>">
+            <%= password_reset ? gettext('reset password') : gettext('verify email') %>
+          </button>
+          <a id="cancel" class="action" href="#"><%= gettext('cancel') %></a>
+      </div>
+  </div>
diff --git a/resources/static/lib/dom-jquery.js b/resources/static/lib/dom-jquery.js
index 860c033277fa2579044a22216426b7ac88bbd8eb..6438fec068304658384f58d1e86e2c0c1c309dcb 100644
--- a/resources/static/lib/dom-jquery.js
+++ b/resources/static/lib/dom-jquery.js
@@ -310,6 +310,24 @@ BrowserID.DOM = ( function() {
          */
         is: function( elementToCheck, type ) {
           return jQuery( elementToCheck ).is( type );
+        },
+
+        /**
+         * Show an element/elements
+         * @method show
+         * @param {selector || element} elementToShow
+         */
+        show: function( elementToShow ) {
+          return jQuery( elementToShow ).show();
+        },
+
+        /**
+         * Hide an element/elements
+         * @method hide
+         * @param {selector || element} elementToHide
+         */
+        hide: function( elementToHide ) {
+          return jQuery( elementToHide ).hide();
         }
 
 
diff --git a/resources/static/pages/add_email_address.js b/resources/static/pages/add_email_address.js
deleted file mode 100644
index 1a001d048122761bff9bdfdc6e130ff3be76dc8f..0000000000000000000000000000000000000000
--- a/resources/static/pages/add_email_address.js
+++ /dev/null
@@ -1,125 +0,0 @@
-/*globals BrowserID: true, $:true */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-BrowserID.addEmailAddress = (function() {
-  "use strict";
-
-  var ANIMATION_TIME=250,
-      bid = BrowserID,
-      user = bid.User,
-      storage = bid.Storage,
-      errors = bid.Errors,
-      pageHelpers = bid.PageHelpers,
-      dom = bid.DOM,
-      token,
-      sc;
-
-  function showError(el, oncomplete) {
-    $(".hint,#signUpForm").hide();
-    $(el).fadeIn(ANIMATION_TIME, oncomplete);
-  }
-
-  function emailRegistrationComplete(oncomplete, info) {
-    function complete(status) {
-      oncomplete && oncomplete(status);
-    }
-
-    var valid = info.valid;
-    if (valid) {
-      emailRegistrationSuccess(info, complete.curry(true));
-    }
-    else {
-      showError("#cannotconfirm", complete.curry(false));
-    }
-  }
-
-  function showRegistrationInfo(info) {
-    dom.setInner(".email", info.email);
-
-    if (info.origin) {
-      dom.setInner(".website", info.origin);
-      $(".siteinfo").show();
-    }
-  }
-
-  function emailRegistrationSuccess(info, oncomplete) {
-    dom.addClass("body", "complete");
-
-    showRegistrationInfo(info);
-
-    setTimeout(function() {
-      pageHelpers.replaceFormWithNotice("#congrats", oncomplete);
-    }, 2000);
-  }
-
-  function userMustEnterPassword(info) {
-    return !!info.needs_password;
-  }
-
-  function verifyWithoutPassword(oncomplete) {
-    user.verifyEmailNoPassword(token,
-      emailRegistrationComplete.curry(oncomplete),
-      pageHelpers.getFailure(errors.verifyEmail, oncomplete)
-    );
-  }
-
-  function verifyWithPassword(oncomplete) {
-    var pass = dom.getInner("#password"),
-        vpass = dom.getInner("#vpassword"),
-        valid = bid.Validation.passwordAndValidationPassword(pass, vpass);
-
-    if(valid) {
-      user.verifyEmailWithPassword(token, pass,
-        emailRegistrationComplete.curry(oncomplete),
-        pageHelpers.getFailure(errors.verifyEmail, oncomplete)
-      );
-    }
-    else {
-      oncomplete && oncomplete(false);
-    }
-  }
-
-  function startVerification(oncomplete) {
-    user.tokenInfo(token, function(info) {
-      if(info) {
-        showRegistrationInfo(info);
-
-        if(userMustEnterPassword(info)) {
-          dom.addClass("body", "enter_password");
-          oncomplete(true);
-        }
-        else {
-          verifyWithoutPassword(oncomplete);
-        }
-      }
-      else {
-        showError("#cannotconfirm");
-        oncomplete(false);
-      }
-    }, pageHelpers.getFailure(errors.getTokenInfo, oncomplete));
-  }
-
-  var Module = bid.Modules.PageModule.extend({
-    start: function(options) {
-      function oncomplete(status) {
-        options.ready && options.ready(status);
-      }
-
-      this.checkRequired(options, "token");
-
-      token = options.token;
-
-      startVerification(oncomplete);
-
-      sc.start.call(this, options);
-    },
-
-    submit: verifyWithPassword
-  });
-
-  sc = Module.sc;
-
-  return Module;
-}());
diff --git a/resources/static/pages/forgot.js b/resources/static/pages/forgot.js
index 529c066fbd866f84e7c3d2f6e4d6e44f476dec58..5d7c9385355f1dbc1eca1ff0bf85b826b867997e 100644
--- a/resources/static/pages/forgot.js
+++ b/resources/static/pages/forgot.js
@@ -9,6 +9,8 @@ BrowserID.forgot = (function() {
   var bid = BrowserID,
       user = bid.User,
       helpers = bid.Helpers,
+      complete = helpers.complete,
+      validation = bid.Validation,
       pageHelpers = bid.PageHelpers,
       cancelEvent = pageHelpers.cancelEvent,
       dom = bid.DOM,
@@ -18,20 +20,24 @@ BrowserID.forgot = (function() {
     // GET RID OF THIS HIDE CRAP AND USE CSS!
     $(".notifications .notification").hide();
 
-    var email = helpers.getAndValidateEmail("#email");
+    var email = helpers.getAndValidateEmail("#email"),
+        pass = dom.getInner("#password"),
+        vpass = dom.getInner("#vpassword"),
+        validPass = email && validation.passwordAndValidationPassword(pass, vpass);
 
-    if (email) {
-      user.requestPasswordReset(email, function onSuccess(info) {
+    if (email && validPass) {
+      user.requestPasswordReset(email, pass, function onSuccess(info) {
         if (info.success) {
           pageHelpers.emailSent(oncomplete);
         }
         else {
           var tooltipEl = info.reason === "throttle" ? "#could_not_add" : "#not_registered";
-          tooltip.showTooltip(tooltipEl, oncomplete);
+          tooltip.showTooltip(tooltipEl);
+          complete(oncomplete);
         }
       }, pageHelpers.getFailure(bid.Errors.requestPasswordReset, oncomplete));
     } else {
-      oncomplete && oncomplete();
+      complete(oncomplete);
     }
   };
 
diff --git a/resources/static/pages/signup.js b/resources/static/pages/signup.js
index 475d53824fef0a7c8366995939d6520720d8468e..3fef51badbfe646ad8f2d9ec523afff178dfd896 100644
--- a/resources/static/pages/signup.js
+++ b/resources/static/pages/signup.js
@@ -12,63 +12,48 @@ BrowserID.signUp = (function() {
       helpers = bid.Helpers,
       pageHelpers = bid.PageHelpers,
       cancelEvent = pageHelpers.cancelEvent,
+      validation = bid.Validation,
       errors = bid.Errors,
       tooltip = BrowserID.Tooltip,
       ANIMATION_SPEED = 250,
       storedEmail = pageHelpers,
       winchan = window.WinChan,
-      verifyEmail,
-      verifyURL;
+      primaryUserInfo,
+      sc;
 
     function showNotice(selector) {
       $(selector).fadeIn(ANIMATION_SPEED);
     }
 
     function authWithPrimary(oncomplete) {
-      pageHelpers.openPrimaryAuth(winchan, verifyEmail, verifyURL, primaryAuthComplete);
+      pageHelpers.openPrimaryAuth(winchan, primaryUserInfo.email, primaryUserInfo.auth, primaryAuthComplete);
 
       oncomplete && oncomplete();
     }
 
     function primaryAuthComplete(error, result, oncomplete) {
-      if(error) {
+      if (error) {
         pageHelpers.showFailure(errors.primaryAuthentication, error, oncomplete);
       }
       else {
         // hey ho, the user is authenticated, re-try the submit.
-        createUser(verifyEmail, oncomplete);
+        createPrimaryUser(primaryUserInfo, oncomplete);
       }
     }
 
-    function createUser(email, oncomplete) {
+    function createPrimaryUser(info, oncomplete) {
       function complete(status) {
         oncomplete && oncomplete(status);
       }
 
-      user.createUser(email, function onComplete(status, info) {
+      user.createPrimaryUser(info, function onComplete(status, info) {
         switch(status) {
-          case "secondary.already_added":
-            $('#registeredEmail').html(email);
-            showNotice(".alreadyRegistered");
-            complete(false);
-            break;
-          case "secondary.verify":
-            pageHelpers.emailSent(complete);
-            break;
-          case "secondary.could_not_add":
-            tooltip.showTooltip("#could_not_add");
-            complete(false);
-            break;
-          case "primary.already_added":
-            // XXX Is this status possible?
-            break;
           case "primary.verified":
             pageHelpers.replaceFormWithNotice("#congrats", complete.bind(null, true));
             break;
           case "primary.verify":
-            verifyEmail = email;
-            verifyURL = info.auth;
-            dom.setInner("#primary_email", email);
+            primaryUserInfo = info;
+            dom.setInner("#primary_email", info.email);
             pageHelpers.replaceInputsWithNotice("#primary_verify", complete.bind(null, false));
             break;
           case "primary.could_not_add":
@@ -80,18 +65,62 @@ BrowserID.signUp = (function() {
       }, pageHelpers.getFailure(errors.createUser, complete));
     }
 
-    function submit(oncomplete) {
-      var email = helpers.getAndValidateEmail("#email");
+    function enterPasswordState(info) {
+      var self=this;
+      self.emailToStage = info.email;
+      self.submit = passwordSubmit;
 
-      function complete(status) {
-        oncomplete && oncomplete(status);
+      dom.addClass("body", "enter_password");
+    }
+
+    function passwordSubmit(oncomplete) {
+      var pass = dom.getInner("#password"),
+          vpass = dom.getInner("#vpassword"),
+          valid = validation.passwordAndValidationPassword(pass, vpass);
+
+      if(valid) {
+        user.createSecondaryUser(this.emailToStage, pass, function(status) {
+          if(status) {
+            pageHelpers.emailSent(oncomplete && oncomplete.curry(true));
+          }
+          else {
+            tooltip.showTooltip("#could_not_add");
+            oncomplete && oncomplete(false);
+          }
+        }, pageHelpers.getFailure(errors.createUser, oncomplete));
+      }
+      else {
+        oncomplete && oncomplete(false);
       }
+    }
+
+    function emailSubmit(oncomplete) {
+      var email = helpers.getAndValidateEmail("#email"),
+          self = this;
 
       if (email) {
-        createUser(email, complete);
+
+        user.isEmailRegistered(email, function(isRegistered) {
+          if(isRegistered) {
+            $('#registeredEmail').html(email);
+            showNotice(".alreadyRegistered");
+            oncomplete && oncomplete(false);
+          }
+          else {
+            user.addressInfo(email, function(info) {
+              if(info.type === "primary") {
+                createPrimaryUser.call(self, info, oncomplete);
+              }
+              else {
+                enterPasswordState.call(self, info);
+                oncomplete && oncomplete(!isRegistered);
+              }
+            }, pageHelpers.getFailure(errors.addressInfo, oncomplete));
+          }
+        }, pageHelpers.getFailure(errors.isEmailRegistered, oncomplete));
       }
       else {
-        complete(false);
+        oncomplete && oncomplete(false);
       }
     }
 
@@ -103,39 +132,45 @@ BrowserID.signUp = (function() {
       if (event.which !== 13) $(".notification").fadeOut(ANIMATION_SPEED);
     }
 
-    function init(config) {
-      config = config || {};
+    var Module = bid.Modules.PageModule.extend({
+      start: function(options) {
+        var self=this;
+        options = options || {};
 
-      if(config.winchan) {
-        winchan = config.winchan;
-      }
+        if (options.winchan) {
+          winchan = options.winchan;
+        }
 
-      $("form input[autofocus]").focus();
+        dom.focus("form input[autofocus]");
 
-      pageHelpers.setupEmail();
+        pageHelpers.setupEmail();
+
+        self.bind("#email", "keyup", onEmailKeyUp);
+        self.click("#back", back);
+        self.click("#authWithPrimary", authWithPrimary);
+
+        sc.start.call(self, options);
+      },
+
+      submit: emailSubmit,
+      // BEGIN TESTING API
+      emailSubmit: emailSubmit,
+      passwordSubmit: passwordSubmit,
+      reset: reset,
+      back: back,
+      authWithPrimary: authWithPrimary,
+      primaryAuthComplete: primaryAuthComplete
+      // END TESTING API
+    });
 
-      dom.bindEvent("#email", "keyup", onEmailKeyUp);
-      dom.bindEvent("form", "submit", cancelEvent(submit));
-      dom.bindEvent("#back", "click", cancelEvent(back));
-      dom.bindEvent("#authWithPrimary", "click", cancelEvent(authWithPrimary));
-    }
 
     // BEGIN TESTING API
     function reset() {
-      dom.unbindEvent("#email", "keyup");
-      dom.unbindEvent("form", "submit");
-      dom.unbindEvent("#back", "click");
-      dom.unbindEvent("#authWithPrimary", "click");
       winchan = window.WinChan;
-      verifyEmail = verifyURL = null;
     }
-
-    init.submit = submit;
-    init.reset = reset;
-    init.back = back;
-    init.authWithPrimary = authWithPrimary;
-    init.primaryAuthComplete = primaryAuthComplete;
     // END TESTING API
 
-    return init;
+    sc = Module.sc;
+
+    return Module;
 }());
diff --git a/resources/static/pages/start.js b/resources/static/pages/start.js
index 34e4c2dec70f3cfdc4588a875a3ad1f4e09198ce..e43661c6d51a1cbff4ec3f7af807fc2dd271ddf4 100644
--- a/resources/static/pages/start.js
+++ b/resources/static/pages/start.js
@@ -24,7 +24,8 @@ $(function() {
       CookieCheck = modules.CookieCheck,
       XHRDelay = modules.XHRDelay,
       XHRDisableForm = modules.XHRDisableForm,
-      ANIMATION_TIME = 500;
+      ANIMATION_TIME = 500,
+      checkCookiePaths = [ "/signin", "/signup", "/forgot", "/add_email_address", "/verify_email_address" ];
 
 
   xhr.init({ time_until_delay: 10 * 1000 });
@@ -41,7 +42,7 @@ $(function() {
   moduleManager.register("xhr_disable_form", XHRDisableForm);
   moduleManager.start("xhr_disable_form");
 
-  if(path && path !== "/") {
+  if(path && (checkCookiePaths.indexOf(path) > -1)) {
     // do a cookie check on every page except the main page.
     moduleManager.register("cookie_check", CookieCheck);
     moduleManager.start("cookie_check", { ready: start });
@@ -66,19 +67,25 @@ $(function() {
       module.start({});
     }
     else if (path === "/signup") {
-      bid.signUp();
+      var module = bid.signUp.create();
+      module.start({});
     }
     else if (path === "/forgot") {
       bid.forgot();
     }
     else if (path === "/add_email_address") {
-      var module = bid.addEmailAddress.create();
+      var module = bid.verifySecondaryAddress.create();
       module.start({
-        token: token
+        token: token,
+        verifyFunction: "verifyEmail"
       });
     }
-    else if(token && path === "/verify_email_address") {
-      bid.verifyEmailAddress(token);
+    else if(path === "/verify_email_address") {
+      var module = bid.verifySecondaryAddress.create();
+      module.start({
+        token: token,
+        verifyFunction: "verifyUser"
+      });
     }
     else {
       // Instead of throwing a hard error here, adding a message to the console
diff --git a/resources/static/pages/verify_email_address.js b/resources/static/pages/verify_email_address.js
deleted file mode 100644
index b53ab9b8f4f47b8d53de3ebdc99551a0c4151b5e..0000000000000000000000000000000000000000
--- a/resources/static/pages/verify_email_address.js
+++ /dev/null
@@ -1,66 +0,0 @@
-/*globals BrowserID: true, $:true */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-(function() {
-  "use strict";
-
-  var bid = BrowserID,
-      errors = bid.Errors,
-      pageHelpers = bid.PageHelpers,
-      token;
-
-  function submit(oncomplete) {
-    var pass = $("#password").val(),
-        vpass = $("#vpassword").val();
-
-    var valid = bid.Validation.passwordAndValidationPassword(pass, vpass);
-
-    if (valid) {
-      bid.Network.completeUserRegistration(token, pass, function onSuccess(registered) {
-        var selector = registered ? "#congrats" : "#cannotcomplete";
-        pageHelpers.replaceFormWithNotice(selector, oncomplete);
-      }, pageHelpers.getFailure(errors.completeUserRegistration, oncomplete));
-    }
-    else {
-      oncomplete && oncomplete();
-    }
-  }
-
-  function init(tok, oncomplete) {
-    $("#signUpForm").bind("submit", pageHelpers.cancelEvent(submit));
-    $(".siteinfo").hide();
-    $("#congrats").hide();
-    token = tok;
-
-    var staged = bid.Storage.getStagedOnBehalfOf();
-    if (staged) {
-      $('.website').html(staged);
-      $('.siteinfo').show();
-    }
-
-    // go get the email address
-    bid.Network.emailForVerificationToken(token, function(info) {
-      if (info) {
-        $('#email').val(info.email);
-        oncomplete && oncomplete();
-      }
-      else {
-        pageHelpers.replaceFormWithNotice("#cannotconfirm", oncomplete);
-      }
-    }, pageHelpers.getFailure(errors.completeUserRegistration, oncomplete));
-  }
-
-  // BEGIN TESTING API
-  function reset() {
-    $("#signUpForm").unbind("submit");
-  }
-
-  init.submit = submit;
-  init.reset = reset;
-  // END TESTING API;
-
-  bid.verifyEmailAddress = init;
-
-}());
diff --git a/resources/static/pages/verify_secondary_address.js b/resources/static/pages/verify_secondary_address.js
new file mode 100644
index 0000000000000000000000000000000000000000..2ccfb0c804ec90c672114c6235fcbefee8b3e0df
--- /dev/null
+++ b/resources/static/pages/verify_secondary_address.js
@@ -0,0 +1,94 @@
+/*globals BrowserID: true, $:true */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+BrowserID.verifySecondaryAddress = (function() {
+  "use strict";
+
+  var ANIMATION_TIME=250,
+      bid = BrowserID,
+      user = bid.User,
+      errors = bid.Errors,
+      pageHelpers = bid.PageHelpers,
+      dom = bid.DOM,
+      helpers = bid.Helpers,
+      complete = helpers.complete,
+      validation = bid.Validation,
+      token,
+      sc,
+      mustAuth,
+      verifyFunction;
+
+  function showError(el, oncomplete) {
+    dom.hide(".hint,#signUpForm");
+    $(el).fadeIn(ANIMATION_TIME, oncomplete);
+  }
+
+  function showRegistrationInfo(info) {
+    dom.setInner("#email", info.email);
+
+    if (info.origin) {
+      dom.setInner(".website", info.origin);
+      dom.show(".siteinfo");
+    }
+  }
+
+  function submit(oncomplete) {
+    var pass = dom.getInner("#password") || undefined,
+        valid = !mustAuth || validation.password(pass);
+
+    if (valid) {
+      user[verifyFunction](token, pass, function(info) {
+        dom.addClass("body", "complete");
+
+        var selector = info.valid ? "#congrats" : "#cannotcomplete";
+        pageHelpers.replaceFormWithNotice(selector, complete.curry(oncomplete, info.valid));
+      }, pageHelpers.getFailure(errors.verifyEmail, oncomplete));
+    }
+    else {
+      complete(oncomplete, false);
+    }
+  }
+
+  function startVerification(oncomplete) {
+    user.tokenInfo(token, function(info) {
+      if(info) {
+        showRegistrationInfo(info);
+
+        mustAuth = info.must_auth;
+
+        if (mustAuth) {
+          dom.addClass("body", "enter_password");
+          complete(oncomplete, true);
+        }
+        else {
+          submit(oncomplete);
+        }
+      }
+      else {
+        showError("#cannotconfirm");
+        complete(oncomplete, false);
+      }
+    }, pageHelpers.getFailure(errors.getTokenInfo, oncomplete));
+  }
+
+  var Module = bid.Modules.PageModule.extend({
+    start: function(options) {
+      this.checkRequired(options, "token", "verifyFunction");
+
+      token = options.token;
+      verifyFunction = options.verifyFunction;
+
+      startVerification(options.ready);
+
+      sc.start.call(this, options);
+    },
+
+    submit: submit
+  });
+
+  sc = Module.sc;
+
+  return Module;
+}());
diff --git a/resources/static/shared/history.js b/resources/static/shared/history.js
index 6cbaa68fe1a7deeeeba5ed898029a78acab8233d..8c7653c3aaa3df1fbf4016d93a89fc45c3bd8b73 100644
--- a/resources/static/shared/history.js
+++ b/resources/static/shared/history.js
@@ -29,6 +29,7 @@ BrowserID.History = (function() {
       return this.current;
     },
 
+    // XXX this should be renamed to pushState
     saveState: function() {
       this.history.push(this.current);
     },
diff --git a/resources/static/shared/modules/page_module.js b/resources/static/shared/modules/page_module.js
index a86f40cf99d4dfafd53bcb26f1a74a257aa43841..3a0186f0ada2dcda42a8ec543134c4f666cee25a 100644
--- a/resources/static/shared/modules/page_module.js
+++ b/resources/static/shared/modules/page_module.js
@@ -51,8 +51,9 @@ BrowserID.Modules.PageModule = (function() {
 
     start: function(options) {
       var self=this;
+      self.options = options || {};
+
       self.bind("form", "submit", cancelEvent(onSubmit));
-      self.click("#thisIsNotMe", self.close.bind(self, "notme"));
     },
 
     stop: function() {
@@ -153,6 +154,7 @@ BrowserID.Modules.PageModule = (function() {
     submit: function() {
     },
 
+    // XXX maybe we should not get rid of this.
     close: function(message) {
       this.destroy();
       if (message) {
diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js
index d1e0cfc1d4391ae2f1408be4985de6ada84683e2..23d92ff38fe92c090c89dc073d6e780ea455151b 100644
--- a/resources/static/shared/network.js
+++ b/resources/static/shared/network.js
@@ -203,16 +203,18 @@ BrowserID.Network = (function() {
     /**
      * Create a new user.  Requires a user to verify identity.
      * @method createUser
-     * @param {string} email - Email address to prepare.
+     * @param {string} email
+     * @param {string} password
      * @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.
      */
-    createUser: function(email, origin, onComplete, onFailure) {
+    createUser: function(email, password, origin, onComplete, onFailure) {
       post({
         url: "/wsapi/stage_user",
         data: {
           email: email,
+          pass: password,
           site : origin
         },
         success: function(status) {
@@ -279,7 +281,7 @@ BrowserID.Network = (function() {
      * Complete user registration, give user a password
      * @method completeUserRegistration
      * @param {string} token - token to register for.
-     * @param {string} password - password to register for account.
+     * @param {string} password
      * @param {function} [onComplete] - Called when complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
@@ -301,7 +303,7 @@ BrowserID.Network = (function() {
      * Call with a token to prove an email address ownership.
      * @method completeEmailRegistration
      * @param {string} token - token proving email ownership.
-     * @param {string} password - password to set if necessary.  If not necessary, set to undefined.
+     * @param {string} password
      * @param {function} [onComplete] - Callback to call when complete.  Called
      * with one boolean parameter that specifies the validity of the token.
      * @param {function} [onFailure] - Called on XHR failure.
@@ -323,13 +325,15 @@ BrowserID.Network = (function() {
     /**
      * Request a password reset for the given email address.
      * @method requestPasswordReset
-     * @param {string} email - email address to reset password for.
+     * @param {string} email
+     * @param {string} password
+     * @param {string} origin
      * @param {function} [onComplete] - Callback to call when complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    requestPasswordReset: function(email, origin, onComplete, onFailure) {
+    requestPasswordReset: function(email, password, origin, onComplete, onFailure) {
       if (email) {
-        Network.createUser(email, origin, onComplete, onFailure);
+        Network.createUser(email, password, origin, onComplete, onFailure);
       } else {
         // TODO: if no email is provided, then what?
         throw "no email provided to password reset";
@@ -439,16 +443,18 @@ BrowserID.Network = (function() {
     /**
      * Add a secondary email to the current user's account.
      * @method addSecondaryEmail
-     * @param {string} email - Email address to add.
-     * @param {string} origin - site user is trying to sign in to.
+     * @param {string} email
+     * @param {string} password
+     * @param {string} origin
      * @param {function} [onComplete] - called when complete.
      * @param {function} [onFailure] - called on xhr failure.
      */
-    addSecondaryEmail: function(email, origin, onComplete, onFailure) {
+    addSecondaryEmail: function(email, password, origin, onComplete, onFailure) {
       post({
         url: "/wsapi/stage_email",
         data: {
           email: email,
+          pass: password,
           site: origin
         },
         success: function(response) {
@@ -659,7 +665,10 @@ BrowserID.Network = (function() {
       withContext(function() {
         try {
           // set a test cookie with a duration of 1 second.
-          // NOTE - The Android 3.3 default browser will still pass this.
+          // NOTE - The Android 3.3 and 4.0 default browsers will still pass
+          // this check.  This causes the Android browsers to only display the
+          // cookies diabled error screen only after the user has entered and
+          // 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;
diff --git a/resources/static/shared/state_machine.js b/resources/static/shared/state_machine.js
index 1b2c90b370af8258cf213fc3167c5b696dde180f..c6dcafdcc6db6f2f1c4a8e58754d0f859b02bcca 100644
--- a/resources/static/shared/state_machine.js
+++ b/resources/static/shared/state_machine.js
@@ -53,6 +53,7 @@ BrowserID.StateMachine = (function() {
       // only save the current state when a new state comes in.
       var cmd = history.getCurrent();
       if(cmd && cmd.save) {
+        // XXX saveState should be renamed to pushState
         history.saveState();
       }
 
diff --git a/resources/static/shared/storage.js b/resources/static/shared/storage.js
index dd5c8ce0f17105f804b70af5a1a03c3d89e851a9..a6a066509d501585d6d5fc6c7390457f064d1f87 100644
--- a/resources/static/shared/storage.js
+++ b/resources/static/shared/storage.js
@@ -3,12 +3,14 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 BrowserID.Storage = (function() {
+  "use strict";
 
   var jwk,
-      ONE_DAY_IN_MS = (1000 * 60 * 60 * 24);
+      ONE_DAY_IN_MS = (1000 * 60 * 60 * 24),
+      storage;
 
   try {
-    var storage = localStorage;
+    storage = localStorage;
   }
   catch(e) {
     // Fx with cookies disabled will except while trying to access
@@ -66,6 +68,18 @@ BrowserID.Storage = (function() {
     storeEmails(emails);
   }
 
+  function addPrimaryEmail(email, obj) {
+    obj = obj || {};
+    obj.type = "primary";
+    addEmail(email, obj);
+  }
+
+  function addSecondaryEmail(email, obj) {
+    obj = obj || {};
+    obj.type = "secondary";
+    addEmail(email, obj);
+  }
+
   function removeEmail(email) {
     var emails = getEmails();
     if(emails[email]) {
@@ -212,7 +226,7 @@ BrowserID.Storage = (function() {
       if (lastState !== currentState) {
         callback();
         lastState = currentState;
-      };
+      }
     }
 
     // IE8 does not have addEventListener, nor does it support storage events.
@@ -350,7 +364,7 @@ BrowserID.Storage = (function() {
 
   function clearUsersComputerOwnershipStatus(userid) {
     try {
-      allInfo = JSON.parse(storage.usersComputer);
+      var allInfo = JSON.parse(storage.usersComputer);
       if (typeof allInfo !== 'object') throw 'bogus';
 
       var userInfo = allInfo[userid];
@@ -435,6 +449,16 @@ BrowserID.Storage = (function() {
      * @method addEmail
      */
     addEmail: addEmail,
+    /**
+     * Add a primary address
+     * @method addPrimaryEmail
+     */
+    addPrimaryEmail: addPrimaryEmail,
+    /**
+     * Add a secondary address
+     * @method addSecondaryEmail
+     */
+    addSecondaryEmail: addSecondaryEmail,
     /**
      * Get all email addresses and their associated key pairs
      * @method getEmails
diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js
index aee3065b955d292649396333666c01dfa03ae811..fb95b25567fa639e3bee1fe83d68ea4cd919600f 100644
--- a/resources/static/shared/user.js
+++ b/resources/static/shared/user.js
@@ -280,27 +280,30 @@ BrowserID.User = (function() {
     /**
      * Create a user account - this creates an user account that must be verified.
      * @method createSecondaryUser
-     * @param {string} email - Email address.
+     * @param {string} email
+     * @param {string} password
      * @param {function} [onComplete] - Called on completion.
      * @param {function} [onFailure] - Called on error.
      */
-    createSecondaryUser: function(email, onComplete, onFailure) {
-      // remember this for later
+    createSecondaryUser: function(email, password, onComplete, onFailure) {
+      // Used on the main site when the user verifies - we try to show them
+      // what URL they came from.
+
+      // XXX - this will have to be updated to either store both the hostname
+      // and the exact URL of the RP or just the URL of the RP and the origin
+      // is extracted from that.
       storage.setStagedOnBehalfOf(User.getHostname());
 
-      network.createUser(email, origin, onComplete, onFailure);
+      network.createUser(email, password, origin, onComplete, onFailure);
     },
 
     /**
-     * Create a user.  Works for both primaries and secondaries.
+     * Create a primary user.
      * @method createUser
-     * @param {string} email
+     * @param {object} info
      * @param {function} onComplete - function to call on complettion.  Called
      * with two parameters - status and info.
      * Status can be:
-     *  secondary.already_added
-     *  secondary.verify
-     *  secondary.could_not_add
      *  primary.already_added
      *  primary.verified
      *  primary.verify
@@ -309,62 +312,23 @@ BrowserID.User = (function() {
      *  info is passed on primary.verify and contains the info necessary to
      *  verify the user with the IdP
      */
-    createUser: function(email, onComplete, onFailure) {
-      User.addressInfo(email, function(info) {
-        User.createUserWithInfo(email, info, onComplete, onFailure);
-      }, onFailure);
-    },
-
-    /**
-     * Attempt to create a user with the info returned from
-     * network.addressInfo.  Attempts to create both primary and secondary
-     * based users depending on info.type.
-     * @method createUserWithInfo
-     * @param {string} email
-     * @param {object} info - contains fields returned from network.addressInfo
-     * @param {function} [onComplete]
-     * @param {function} [onFailure]
-     */
-    createUserWithInfo: function(email, info, onComplete, onFailure) {
-      function attemptAddSecondary(email, info) {
-        if (info.known) {
-          onComplete("secondary.already_added");
-        }
-        else {
-          User.createSecondaryUser(email, function(success) {
-            if (success) {
-              onComplete("secondary.verify");
+    createPrimaryUser: function(info, onComplete, onFailure) {
+      var email = info.email;
+      User.provisionPrimaryUser(email, info, function(status, provInfo) {
+        if (status === "primary.verified") {
+          network.authenticateWithAssertion(email, provInfo.assertion, function(status) {
+            if (status) {
+              onComplete("primary.verified");
             }
             else {
-              onComplete("secondary.could_not_add");
+              onComplete("primary.could_not_add");
             }
           }, onFailure);
         }
-      }
-
-      function attemptAddPrimary(email, info) {
-        User.provisionPrimaryUser(email, info, function(status, provInfo) {
-          if (status === "primary.verified") {
-            network.authenticateWithAssertion(email, provInfo.assertion, function(status) {
-              if (status) {
-                onComplete("primary.verified");
-              }
-              else {
-                onComplete("primary.could_not_add");
-              }
-            }, onFailure);
-          }
-          else {
-            onComplete(status, provInfo);
-          }
-        }, onFailure);
-      }
-
-      if (info.type === 'secondary') {
-        attemptAddSecondary(email, info);
-      } else {
-        attemptAddPrimary(email, info);
-      }
+        else {
+          onComplete(status, provInfo);
+        }
+      }, onFailure);
     },
 
     /**
@@ -533,8 +497,10 @@ BrowserID.User = (function() {
      * Verify a user
      * @method verifyUser
      * @param {string} token - token to verify.
-     * @param {string} password - password to set for account.
-     * @param {function} [onComplete] - Called to give status updates.
+     * @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.
      */
     verifyUser: function(token, password, onComplete, onFailure) {
@@ -542,9 +508,14 @@ BrowserID.User = (function() {
         var invalidInfo = { valid: false };
         if (info) {
           network.completeUserRegistration(token, password, function (valid) {
-            info.valid = valid;
-            storage.setStagedOnBehalfOf("");
-            if (onComplete) onComplete(info);
+            var result = invalidInfo;
+
+            if(valid) {
+              result = _.extend({ valid: valid, origin: storage.getStagedOnBehalfOf() }, info);
+              storage.setStagedOnBehalfOf("");
+            }
+
+            complete(onComplete, result);
           }, onFailure);
         } else if (onComplete) {
           onComplete(invalidInfo);
@@ -592,17 +563,18 @@ BrowserID.User = (function() {
     /**
      * Request a password reset for the given email address.
      * @method requestPasswordReset
-     * @param {string} email - email address to reset password for.
+     * @param {string} email
+     * @param {string} password
      * @param {function} [onComplete] - Callback to call when complete, called
      * with a single object, info.
      *    info.status {boolean} - true or false whether request was successful.
      *    info.reason {string} - if status false, reason of failure.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    requestPasswordReset: function(email, onComplete, onFailure) {
+    requestPasswordReset: function(email, password, onComplete, onFailure) {
       User.isEmailRegistered(email, function(registered) {
         if (registered) {
-          network.requestPasswordReset(email, origin, function(reset) {
+          network.requestPasswordReset(email, password, origin, function(reset) {
             var status = {
               success: reset
             };
@@ -829,12 +801,13 @@ BrowserID.User = (function() {
      * does not add the new email address/keypair to the local list of
      * valid identities.
      * @method addEmail
-     * @param {string} email - Email address.
+     * @param {string} email
+     * @param {string} password
      * @param {function} [onComplete] - Called on successful completion.
      * @param {function} [onFailure] - Called on error.
      */
-    addEmail: function(email, onComplete, onFailure) {
-      network.addSecondaryEmail(email, origin, function(added) {
+    addEmail: function(email, password, onComplete, onFailure) {
+      network.addSecondaryEmail(email, password, origin, function(added) {
         if (added) storage.setStagedOnBehalfOf(User.getHostname());
 
         // we no longer send the keypair, since we will certify it later.
@@ -842,6 +815,27 @@ BrowserID.User = (function() {
       }, onFailure);
     },
 
+    /**
+     * Check whether a password is needed to add a secondary email address to
+     * an already existing account.
+     * @method passwordNeededToAddSecondaryEmail
+     * @param {function} [onComplete] - Called on successful completion, called
+     * with true if password is needed, false otw.
+     * @param {function} [onFailure] - Called on error.
+     */
+    passwordNeededToAddSecondaryEmail: function(onComplete, onFailure) {
+      var emails = storage.getEmails(),
+          passwordNeeded = true;
+
+      for(var key in emails) {
+        if(emails[key].type === "secondary") {
+          passwordNeeded = false;
+        }
+      }
+
+      complete(onComplete, passwordNeeded);
+    },
+
     /**
      * Wait for the email registration to complete
      * @method waitForEmailValidation
@@ -865,23 +859,17 @@ BrowserID.User = (function() {
      * Verify a users email address given by the token
      * @method verifyEmail
      * @param {string} token
+     * @param {string} password
      * @param {function} [onComplete] - Called on completion.
      *   Called with an object with valid, email, and origin if valid, called
-     *   with only valid otw.
+     *   with valid=false otw.
      * @param {function} [onFailure] - Called on error.
      */
-    verifyEmailNoPassword: function(token, onComplete, onFailure) {
-      User.verifyEmailWithPassword(token, undefined, onComplete, onFailure);
-    },
-
-    verifyEmailWithPassword: function(token, pass, onComplete, onFailure) {
-      function complete(status) {
-        onComplete && onComplete(status);
-      }
+    verifyEmail: function(token, password, onComplete, onFailure) {
       network.emailForVerificationToken(token, function (info) {
         var invalidInfo = { valid: false };
         if (info) {
-          network.completeEmailRegistration(token, pass, function (valid) {
+          network.completeEmailRegistration(token, password, function (valid) {
             var result = invalidInfo;
 
             if(valid) {
@@ -889,10 +877,10 @@ BrowserID.User = (function() {
               storage.setStagedOnBehalfOf("");
             }
 
-            complete(result);
+            complete(onComplete, result);
           }, onFailure);
         } else {
-          complete(invalidInfo);
+          complete(onComplete, invalidInfo);
         }
       }, onFailure);
     },
@@ -1035,7 +1023,9 @@ BrowserID.User = (function() {
           sortedIdentities = [];
 
       for(var key in identities) {
-        sortedIdentities.push({ address: key, info: identities[key] });
+        if(identities.hasOwnProperty(key)) {
+          sortedIdentities.push({ address: key, info: identities[key] });
+        }
       }
 
       sortedIdentities.sort(function(a, b) {
diff --git a/resources/static/test/cases/controllers/actions.js b/resources/static/test/cases/controllers/actions.js
index 0e0881e2748005970320b4f1d6ac170276af0a04..11c58cd960ce705d77743c6f011ba3b466e0e1a1 100644
--- a/resources/static/test/cases/controllers/actions.js
+++ b/resources/static/test/cases/controllers/actions.js
@@ -84,7 +84,7 @@
   asyncTest("doCannotVerifyRequiredPrimary - show the error screen", function() {
     createController({
       ready: function() {
-        controller.doCannotVerifyRequiredPrimary({ email: "testuser@testuser.com"});
+        controller.doCannotVerifyRequiredPrimary({ email: TEST_EMAIL});
 
         testHelpers.testErrorVisible();
         start();
@@ -112,5 +112,26 @@
     testActionStartsModule('doGenerateAssertion', { email: TEST_EMAIL }, "generate_assertion");
   });
 
+
+  asyncTest("doStageUser with successful creation - trigger user_staged", function() {
+    createController({
+      ready: function() {
+        var email;
+        testHelpers.register("user_staged", function(msg, info) {
+          email = info.email;
+        });
+
+        controller.doStageUser({ email: TEST_EMAIL, password: "password", ready: function(status) {
+          equal(status, true, "correct status");
+          equal(email, TEST_EMAIL, "user successfully staged");
+          start();
+        }});
+      }
+    });
+  });
+
+  asyncTest("doForgotPassword - call the set_password controller with reset_password true", function() {
+    testActionStartsModule('doForgotPassword', { email: TEST_EMAIL }, "set_password");
+  });
 }());
 
diff --git a/resources/static/test/cases/controllers/add_email.js b/resources/static/test/cases/controllers/add_email.js
index b18696f31079038a463fff7341c767e4901a2ab8..b490b519d105fb666a0014bb403a8583eccfd672 100644
--- a/resources/static/test/cases/controllers/add_email.js
+++ b/resources/static/test/cases/controllers/add_email.js
@@ -10,6 +10,7 @@
       el = $("body"),
       bid = BrowserID,
       user = bid.User,
+      storage = bid.Storage,
       xhr = bid.Mocks.xhr,
       modules = bid.Modules,
       testHelpers = bid.TestHelpers,
@@ -55,7 +56,7 @@
     ok($("#newEmail").val(), "testuser@testuser.com", "email prepopulated");
   });
 
-  asyncTest("addEmail with valid unknown secondary email", function() {
+  asyncTest("addEmail with first valid unknown secondary email - trigger add_email_submit_with_secondary", function() {
     createController();
     xhr.useResult("unknown_secondary");
 
@@ -63,21 +64,38 @@
 
     $("#newEmail").val("unregistered@testuser.com");
 
-    register("email_staged", function(msg, info) {
-      equal(info.email, "unregistered@testuser.com", "email_staged called with correct email");
+    register("add_email_submit_with_secondary", function(msg, info) {
+      equal(info.email, "unregistered@testuser.com", "add_email_submit_with_secondary called with correct email");
       start();
     });
 
     controller.addEmail();
   });
 
-  asyncTest("addEmail with valid unknown secondary email with leading/trailing whitespace", function() {
+  asyncTest("addEmail with second valid unknown secondary email - trigger add_email_submit_with_secondary", function() {
+    createController();
+    xhr.useResult("unknown_secondary");
+
+    equal($("#addEmail").length, 1, "control rendered correctly");
+
+    $("#newEmail").val("unregistered@testuser.com");
+
+    register("add_email_submit_with_secondary", function(msg, info) {
+      equal(info.email, "unregistered@testuser.com", "add_email_submit_with_secondary called with correct email");
+      start();
+    });
+
+    storage.addSecondaryEmail("testuser@testuser.com");
+    controller.addEmail();
+  });
+
+  asyncTest("addEmail with valid unknown secondary email with leading/trailing whitespace - allows address, triggers add_email_submit_with_secondary", function() {
     createController();
     xhr.useResult("unknown_secondary");
 
     $("#newEmail").val("   unregistered@testuser.com  ");
-    register("email_staged", function(msg, info) {
-      equal(info.email, "unregistered@testuser.com", "email_staged called with correct email");
+    register("add_email_submit_with_secondary", function(msg, info) {
+      equal(info.email, "unregistered@testuser.com", "add_email_submit_with_secondary called with correct email");
       start();
     });
     controller.addEmail();
@@ -88,12 +106,12 @@
 
     $("#newEmail").val("unregistered");
     var handlerCalled = false;
-    register("email_staged", function(msg, info) {
+    register("add_email_submit_with_secondary", function(msg, info) {
       handlerCalled = true;
-      ok(false, "email_staged should not be called on invalid email");
+      ok(false, "add_email_submit_with_secondary should not be called on invalid email");
     });
     controller.addEmail(function() {
-      equal(handlerCalled, false, "the email_staged handler should have never been called");
+      equal(handlerCalled, false, "the add_email_submit_with_secondary handler should have never been called");
       start();
     });
   });
@@ -103,8 +121,8 @@
 
     $("#newEmail").val("registered@testuser.com");
 
-    register("email_staged", function(msg, info) {
-      ok(false, "unexpected email_staged message");
+    register("add_email_submit_with_secondary", function(msg, info) {
+      ok(false, "unexpected add_email_submit_with_secondary message");
     });
 
     // simulate the email being already added.
@@ -119,13 +137,13 @@
     });
   });
 
-  asyncTest("addEmail with secondary email belonging to another user - allows for account consolidation", function() {
+  asyncTest("addEmail with first secondary email belonging to another user - allows for account consolidation", function() {
     createController();
     xhr.useResult("known_secondary");
 
     $("#newEmail").val("registered@testuser.com");
-    register("email_staged", function(msg, info) {
-      equal(info.email, "registered@testuser.com", "email_staged called with correct email");
+    register("add_email_submit_with_secondary", function(msg, info) {
+      equal(info.email, "registered@testuser.com", "add_email_submit_with_secondary called with correct email");
       start();
     });
     controller.addEmail();
diff --git a/resources/static/test/cases/controllers/authenticate.js b/resources/static/test/cases/controllers/authenticate.js
index dedd5352ca678c405250f5e39f0be16168d3648b..e410de5e57adfa512fbdd34e96339a1528a49930 100644
--- a/resources/static/test/cases/controllers/authenticate.js
+++ b/resources/static/test/cases/controllers/authenticate.js
@@ -80,29 +80,32 @@
   });
 
   function testUserUnregistered() {
-    register("create_user", function() {
-      ok(true, "email was valid, user not registered");
+    register("new_user", function(msg, info, rehydrate) {
+      ok(info.email, "new_user triggered with info.email");
+      // rehydration email used to go back to authentication controller if
+      // the user cancels one of the next steps.
+      ok(rehydrate.email, "new_user triggered with rehydrate.email");
       start();
     });
 
     controller.checkEmail();
   }
 
-  asyncTest("checkEmail with unknown secondary email, expect 'create_user' message", function() {
+  asyncTest("checkEmail with unknown secondary email - 'new_user' message", function() {
     $("#email").val("unregistered@testuser.com");
     xhr.useResult("unknown_secondary");
 
     testUserUnregistered();
   });
 
-  asyncTest("checkEmail with email with leading/trailing whitespace, user not registered, expect 'create_user' message", function() {
+  asyncTest("checkEmail with email with leading/trailing whitespace, user not registered - 'new_user' message", function() {
     $("#email").val("    unregistered@testuser.com   ");
     xhr.useResult("unknown_secondary");
 
     testUserUnregistered();
   });
 
-  asyncTest("checkEmail with normal email, user registered, expect 'enter_password' message", function() {
+  asyncTest("checkEmail with normal email, user registered - 'enter_password' message", function() {
     $("#email").val("registered@testuser.com");
     xhr.useResult("known_secondary");
 
@@ -114,7 +117,7 @@
     controller.checkEmail();
   });
 
-  asyncTest("checkEmail with email that has IdP support, expect 'primary_user' message", function() {
+  asyncTest("checkEmail with email that has IdP support - 'primary_user' message", function() {
     $("#email").val("unregistered@testuser.com");
     xhr.useResult("primary");
 
@@ -165,8 +168,8 @@
     $("#email").val("unregistered@testuser.com");
     xhr.useResult("unknown_secondary");
 
-    register("user_staged", function(msg, info) {
-      equal(info.email, "unregistered@testuser.com", "user_staged with correct email triggered");
+    register("new_user", function(msg, info) {
+      equal(info.email, "unregistered@testuser.com", "new_user with correct email triggered");
       start();
     });
 
@@ -177,43 +180,12 @@
     $("#email").val("unregistered");
 
     var handlerCalled = false;
-    register("user_staged", function(msg, info) {
+    register("new_user", function(msg, info) {
       handlerCalled = true;
     });
 
     controller.createUser(function() {
-      equal(handlerCalled, false, "bad jiji, user_staged should not have been called with invalid email");
-      start();
-    });
-  });
-
-  asyncTest("createUser with valid email but throttling", function() {
-    $("#email").val("unregistered@testuser.com");
-
-    var handlerCalled = false;
-    register("user_staged", function(msg, info) {
-      handlerCalled = true;
-    });
-
-    xhr.useResult("throttle");
-    controller.createUser(function() {
-      equal(handlerCalled, false, "bad jiji, user_staged should not have been called with throttling");
-      equal(bid.Tooltip.shown, true, "tooltip is shown");
-      start();
-    });
-  });
-
-  asyncTest("createUser with valid email, XHR error", function() {
-    $("#email").val("unregistered@testuser.com");
-
-    var handlerCalled = false;
-    register("user_staged", function(msg, info) {
-      handlerCalled = true;
-    });
-
-    xhr.useResult("ajaxError");
-    controller.createUser(function() {
-      equal(handlerCalled, false, "bad jiji, user_staged should not have been called with XHR error");
+      equal(handlerCalled, false, "bad jiji, new_user should not have been called with invalid email");
       start();
     });
   });
diff --git a/resources/static/test/cases/controllers/forgot_password.js b/resources/static/test/cases/controllers/forgot_password.js
index 77c24dd8939eb7c3f1898178a051febb00ca82e2..d81d8b1de331c895aab433830b56b639d8744411 100644
--- a/resources/static/test/cases/controllers/forgot_password.js
+++ b/resources/static/test/cases/controllers/forgot_password.js
@@ -40,9 +40,9 @@
     equal($("#email").val(), "registered@testuser.com", "email prefilled");
   });
 
-  asyncTest("resetPassword raises 'reset_password' with email address", function() {
-    register("reset_password", function(msg, info) {
-      equal(info.email, "registered@testuser.com", "reset_password raised with correct email address");
+  asyncTest("resetPassword raises 'password_reset' with email address", function() {
+    register("password_reset", function(msg, info) {
+      equal(info.email, "registered@testuser.com", "password_reset raised with correct email address");
       start();
     });
 
diff --git a/resources/static/test/cases/controllers/pick_email.js b/resources/static/test/cases/controllers/pick_email.js
index 7b560c6b5e4df29a3990cb0ee95b857bc849af4e..555d2f4631c63ae0c9949339feab0e389c180e2c 100644
--- a/resources/static/test/cases/controllers/pick_email.js
+++ b/resources/static/test/cases/controllers/pick_email.js
@@ -137,5 +137,16 @@
     equal($("#email_0").is(":checked"), true, "radio button is correctly selected");
   });
 
+  asyncTest("click on not me button - trigger notme message", function() {
+    createController();
+
+    register("notme", function(msg, info) {
+      ok(true, "notme triggered");
+      start();
+    });
+
+    $("#thisIsNotMe").click();
+  });
+
 }());
 
diff --git a/resources/static/test/cases/controllers/required_email.js b/resources/static/test/cases/controllers/required_email.js
index 5a92da544ebd7de3e4a013ac63f7138dfed7475b..5251469d8b86069c82f8333ffe99ab1acca70ad4 100644
--- a/resources/static/test/cases/controllers/required_email.js
+++ b/resources/static/test/cases/controllers/required_email.js
@@ -423,18 +423,18 @@
 
   });
 
-  asyncTest("verifyAddress of authenticated user, address belongs to another user - redirects to 'email_staged'", function() {
+  asyncTest("verifyAddress of authenticated user, secondary address belongs to another user - redirects to 'add_email_submit_with_secondary'", function() {
     var email = "registered@testuser.com";
     xhr.useResult("known_secondary");
 
-    testMessageReceived(email, "email_staged");
+    testMessageReceived(email, "add_email_submit_with_secondary");
   });
 
-  asyncTest("verifyAddress of authenticated user, unknown address - redirects to 'email_staged'", function() {
+  asyncTest("verifyAddress of authenticated user, unknown address - redirects to 'add_email_submit_with_secondary'", function() {
     var email = "unregistered@testuser.com";
     xhr.useResult("unknown_secondary");
 
-    testMessageReceived(email, "email_staged");
+    testMessageReceived(email, "add_email_submit_with_secondary");
   });
 
   asyncTest("verifyAddress of un-authenticated user, forgot password - redirect to 'forgot_password'", function() {
diff --git a/resources/static/test/cases/controllers/set_password.js b/resources/static/test/cases/controllers/set_password.js
new file mode 100644
index 0000000000000000000000000000000000000000..bfdda1c0bdee52084d84a0eb773fcf7898e1678a
--- /dev/null
+++ b/resources/static/test/cases/controllers/set_password.js
@@ -0,0 +1,69 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+(function() {
+  "use strict";
+
+  var controller,
+      el = $("body"),
+      bid = BrowserID,
+      testHelpers = bid.TestHelpers,
+      register = testHelpers.register,
+      controller;
+
+  function createController(options) {
+    controller = bid.Modules.SetPassword.create();
+    controller.start(options);
+  }
+
+  module("controllers/set_password", {
+    setup: function() {
+      testHelpers.setup();
+      createController();
+    },
+
+    teardown: function() {
+      controller.destroy();
+      testHelpers.teardown();
+    }
+  });
+
+
+  test("create with no options - show template, user must verify email", function() {
+    ok($("#set_password").length, "set_password template added");
+    equal($("#verify_user").length, 1, "correct button shown");
+  });
+
+  test("create with password_reset option - show template, show reset password button", function() {
+    controller.destroy();
+    createController({ password_reset: true });
+    ok($("#set_password").length, "set_password template added");
+    equal($("#password_reset").length, 1, "correct button shown");
+  });
+
+  asyncTest("submit with good password/vpassword - password_set message raised", function() {
+    $("#password").val("password");
+    $("#vpassword").val("password");
+
+    var password;
+    register("password_set", function(msg, info) {
+      password = info.password;
+    });
+
+    controller.submit(function() {
+      equal(password, "password", "password_set message raised with correct password");
+      start();
+    });
+  });
+
+  asyncTest("cancel - cancel_state message raised", function() {
+    register("cancel_state", function(msg, info) {
+      ok(true, "state cancelled");
+      start();
+    });
+
+    $("#cancel").click();
+  });
+}());
diff --git a/resources/static/test/cases/pages/forgot.js b/resources/static/test/cases/pages/forgot.js
index 22491ef900b829917dda7841d34b89671b0a492f..d1b1466193204d8a576cd6fc0a99bdc23ab57b39 100644
--- a/resources/static/test/cases/pages/forgot.js
+++ b/resources/static/test/cases/pages/forgot.js
@@ -24,60 +24,99 @@
     }
   });
 
-  function testEmailNotSent(extraTests) {
+  function testEmailNotSent(config) {
+    config = config || {};
     bid.forgot.submit(function() {
       equal($(".emailsent").is(":visible"), false, "email not sent");
-      if (extraTests) extraTests();
+      if(config.checkTooltip !== false) testHelpers.testTooltipVisible();
+      if (config.ready) config.ready();
       else start();
     });
   }
 
   asyncTest("requestPasswordReset with invalid email", function() {
     $("#email").val("invalid");
+    $("#password,#vpassword").val("password");
 
     xhr.useResult("invalid");
 
     testEmailNotSent();
   });
 
-  asyncTest("requestPasswordReset with known email", function() {
+  asyncTest("requestPasswordReset with known email, happy case - show email sent notice", function() {
     $("#email").val("registered@testuser.com");
+    $("#password,#vpassword").val("password");
+
     bid.forgot.submit(function() {
       ok($(".emailsent").is(":visible"), "email sent successfully");
       start();
     });
   });
 
-  asyncTest("requestPasswordReset with known email with leading/trailing whitespace", function() {
+  asyncTest("requestPasswordReset with known email with leading/trailing whitespace - show email sent notice", function() {
     $("#email").val("   registered@testuser.com  ");
+    $("#password,#vpassword").val("password");
+
     bid.forgot.submit(function() {
       ok($(".emailsent").is(":visible"), "email sent successfully");
       start();
     });
   });
 
+  asyncTest("requestPasswordReset with missing password", function() {
+    $("#email").val("unregistered@testuser.com");
+    $("#vpassword").val("password");
+
+    testEmailNotSent();
+  });
+
+  asyncTest("requestPasswordReset with too short of a password", function() {
+    $("#email").val("unregistered@testuser.com");
+    $("#password,#vpassword").val("fail");
+
+    testEmailNotSent();
+  });
+
+  asyncTest("requestPasswordReset with too long of a password", function() {
+    $("#email").val("unregistered@testuser.com");
+    $("#password,#vpassword").val(testHelpers.generateString(81));
+
+    testEmailNotSent();
+  });
+
+  asyncTest("requestPasswordReset with missing vpassword", function() {
+    $("#email").val("unregistered@testuser.com");
+    $("#password").val("password");
+
+    testEmailNotSent();
+  });
+
   asyncTest("requestPasswordReset with unknown email", function() {
     $("#email").val("unregistered@testuser.com");
+    $("#password,#vpassword").val("password");
 
     testEmailNotSent();
   });
 
   asyncTest("requestPasswordReset with throttling", function() {
-    xhr.useResult("throttle");
-
-    $("#email").val("throttled@testuser.com");
+    $("#email").val("registered@testuser.com");
+    $("#password,#vpassword").val("password");
 
+    xhr.useResult("throttle");
     testEmailNotSent();
   });
 
   asyncTest("requestPasswordReset with XHR Error", function() {
-    xhr.useResult("ajaxError");
-
     $("#email").val("testuser@testuser.com");
+    $("#password,#vpassword").val("password");
 
-    testEmailNotSent(function() {
-      testHelpers.testErrorVisible();
-      start();
+    xhr.useResult("ajaxError");
+    testEmailNotSent({
+      ready: function() {
+        testHelpers.testErrorVisible();
+        start();
+      },
+      checkTooltip: false
     });
   });
 
diff --git a/resources/static/test/cases/pages/signup.js b/resources/static/test/cases/pages/signup.js
index 6fe113b2b6ad8e6c5f08a01da9a3d7bc1733a6f7..92c2cd00573462efdfb27549a73f4711ab56fc7f 100644
--- a/resources/static/test/cases/pages/signup.js
+++ b/resources/static/test/cases/pages/signup.js
@@ -13,7 +13,8 @@
       WinChanMock = bid.Mocks.WinChan,
       testHelpers = bid.TestHelpers,
       provisioning = bid.Mocks.Provisioning,
-      winchan;
+      winchan,
+      controller;
 
   module("pages/signup", {
     setup: function() {
@@ -22,18 +23,20 @@
       $(".emailsent").hide();
       $(".notification").hide()
       winchan = new WinChanMock();
-      bid.signUp({
+      controller = bid.signUp.create();
+      controller.start({
         winchan: winchan
       });
     },
     teardown: function() {
       testHelpers.teardown();
-      bid.signUp.reset();
+      controller.reset();
+      controller.destroy();
     }
   });
 
-  function testNotRegistered(extraTests) {
-    bid.signUp.submit(function(status) {
+  function testPasswordNotShown(extraTests) {
+    controller.submit(function(status) {
       strictEqual(status, false, "address was not registered");
       equal($(".emailsent").is(":visible"), false, "email not sent, notice not visible");
 
@@ -42,64 +45,69 @@
     });
   }
 
-  asyncTest("signup with valid unregistered secondary email", function() {
-    xhr.useResult("unknown_secondary");
-
+  asyncTest("signup with valid unregistered secondary email - show password", function() {
     $("#email").val("unregistered@testuser.com");
 
-    bid.signUp.submit(function() {
-      equal($(".emailsent").is(":visible"), true, "email sent, notice visible");
+    controller.submit(function() {
+      equal($("body").hasClass("enter_password"), true, "new email, password section shown");
+
       start();
     });
   });
 
-  asyncTest("signup with valid unregistered email with leading/trailing whitespace", function() {
-    xhr.useResult("unknown_secondary");
 
+  asyncTest("submit with valid unregistered email with leading/trailing whitespace", function() {
     $("#email").val(" unregistered@testuser.com ");
 
-    bid.signUp.submit(function() {
-      equal($(".emailsent").is(":visible"), true, "email sent, notice visible");
+    controller.submit(function() {
+      equal($("body").hasClass("enter_password"), true, "new email, password section shown");
       start();
     });
   });
 
-  asyncTest("signup with valid registered email", function() {
-    xhr.useResult("known_secondary");
+  asyncTest("submit with valid registered email", function() {
     $("#email").val("registered@testuser.com");
 
-    testNotRegistered();
+    testPasswordNotShown();
   });
 
-  asyncTest("signup with invalid email address", function() {
+  asyncTest("submit with invalid email address", function() {
     $("#email").val("invalid");
 
-    testNotRegistered();
+    testPasswordNotShown();
   });
 
-  asyncTest("signup with throttling", function() {
-    xhr.useResult("throttle");
-
+  asyncTest("submit with XHR error", function() {
+    xhr.useResult("ajaxError");
     $("#email").val("unregistered@testuser.com");
 
-    testNotRegistered();
+    testPasswordNotShown(function() {
+      testHelpers.testErrorVisible();
+    });
   });
 
-  asyncTest("signup with XHR error", function() {
-    xhr.useResult("invalid");
+
+  asyncTest("passwordSubmit with throttling", function() {
     $("#email").val("unregistered@testuser.com");
+    $("#password, #vpassword").val("password");
 
-    testNotRegistered(function() {
-      testHelpers.testErrorVisible();
+    xhr.useResult("throttle");
+    controller.passwordSubmit(function(userStaged) {
+      equal(userStaged, false, "email throttling took effect, user not staged");
+      start();
     });
   });
 
-  asyncTest("signup with unregistered secondary email and cancel button pressed", function() {
-    xhr.useResult("unknown_secondary");
+  asyncTest("passwordSubmit happy case, check back button too", function() {
     $("#email").val("unregistered@testuser.com");
+    $("#password, #vpassword").val("password");
 
-    bid.signUp.submit(function() {
-      bid.signUp.back(function() {
+    controller.passwordSubmit(function(userStaged) {
+      equal(userStaged, true, "user has been staged");
+      equal($(".emailsent").is(":visible"), true, "email sent, notice visible");
+
+      // check back button
+      controller.back(function() {
         equal($(".notification:visible").length, 0, "no notifications are visible - visible: " + $(".notification:visible").attr("id"));
         ok($(".forminputs:visible").length, "form inputs are again visible");
         equal($("#email").val(), "unregistered@testuser.com", "email address restored");
@@ -108,6 +116,7 @@
     });
   });
 
+
   asyncTest("signup with primary email address, provisioning failure - expect error screen", function() {
     xhr.useResult("primary");
     $("#email").val("unregistered@testuser.com");
@@ -116,7 +125,7 @@
       msg: "doowap"
     });
 
-    bid.signUp.submit(function(status) {
+    controller.submit(function(status) {
       equal(status, false, "provisioning failure, status false");
       testHelpers.testErrorVisible();
       start();
@@ -128,7 +137,7 @@
     $("#email").val("unregistered@testuser.com");
     provisioning.setStatus(provisioning.AUTHENTICATED);
 
-    bid.signUp.submit(function(status) {
+    controller.submit(function(status) {
       equal(status, true, "primary addition success - true status");
       equal($("#congrats:visible").length, 1, "success notification is visible");
       start();
@@ -139,7 +148,7 @@
     xhr.useResult("primary");
     $("#email").val("unregistered@testuser.com");
 
-    bid.signUp.submit(function(status) {
+    controller.submit(function(status) {
       equal($("#primary_verify:visible").length, 1, "success notification is visible");
       equal($("#primary_email").text(), "unregistered@testuser.com", "correct email shown");
       equal(status, false, "user must authenticate, some action needed.");
@@ -151,8 +160,8 @@
     xhr.useResult("primary");
     $("#email").val("unregistered@testuser.com");
 
-    bid.signUp.submit(function(status) {
-      bid.signUp.authWithPrimary(function() {
+    controller.submit(function(status) {
+      controller.authWithPrimary(function() {
         ok(winchan.oncomplete, "winchan set up");
         start();
       });
@@ -160,7 +169,7 @@
   });
 
   asyncTest("primaryAuthComplete with error, expect incorrect status", function() {
-    bid.signUp.primaryAuthComplete("error", "", function(status) {
+    controller.primaryAuthComplete("error", "", function(status) {
       equal(status, false, "correct status for could not complete");
       testHelpers.testErrorVisible();
       start();
@@ -171,15 +180,15 @@
     xhr.useResult("primary");
     $("#email").val("unregistered@testuser.com");
 
-    bid.signUp.submit(function(status) {
-      bid.signUp.authWithPrimary(function() {
+    controller.submit(function(status) {
+      controller.authWithPrimary(function() {
         // In real life the user would now be authenticated.
         provisioning.setStatus(provisioning.AUTHENTICATED);
 
         // Before primaryAuthComplete is called, we reset the user caches to
         // force re-fetching of what could have been stale user data.
         user.resetCaches();
-        bid.signUp.primaryAuthComplete(null, "success", function(status) {
+        controller.primaryAuthComplete(null, "success", function(status) {
           equal(status, true, "correct status");
           equal($("#congrats:visible").length, 1, "success notification is visible");
           start();
diff --git a/resources/static/test/cases/pages/verify_email_address_test.js b/resources/static/test/cases/pages/verify_email_address_test.js
deleted file mode 100644
index c11ae2574bd2b46e071d12037a981eed2ca1907c..0000000000000000000000000000000000000000
--- a/resources/static/test/cases/pages/verify_email_address_test.js
+++ /dev/null
@@ -1,148 +0,0 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
-/*global test: true, start: true, module: true, ok: true, equal: true, BrowserID:true */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-(function() {
-  "use strict";
-
-  var bid = BrowserID,
-      network = bid.Network,
-      storage = bid.Storage,
-      xhr = bid.Mocks.xhr,
-      testHelpers = bid.TestHelpers,
-      testTooltipVisible = testHelpers.testTooltipVisible,
-      validToken = true;
-
-  module("pages/verify_email_address", {
-    setup: function() {
-      testHelpers.setup();
-      bid.Renderer.render("#page_head", "site/verify_email_address", {});
-    },
-    teardown: function() {
-      testHelpers.teardown();
-    }
-  });
-
-  asyncTest("verifyEmailAddress with good token and site", function() {
-    storage.setStagedOnBehalfOf("browserid.org");
-
-    bid.verifyEmailAddress("token", function() {
-      equal($("#email").val(), "testuser@testuser.com", "email set");
-      ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
-      equal($(".website:nth(0)").text(), "browserid.org", "origin is updated");
-      start();
-    });
-  });
-
-  asyncTest("verifyEmailAddress with good token and nosite", function() {
-    $(".siteinfo").hide();
-    storage.setStagedOnBehalfOf("");
-
-    bid.verifyEmailAddress("token", function() {
-      equal($("#email").val(), "testuser@testuser.com", "email set");
-      equal($(".siteinfo").is(":visible"), false, "siteinfo is not visible without having it");
-      equal($(".siteinfo .website").text(), "", "origin is not updated");
-      start();
-    });
-  });
-
-  asyncTest("verifyEmailAddress with bad token", function() {
-    xhr.useResult("invalid");
-
-    bid.verifyEmailAddress("token", function() {
-      ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible");
-      start();
-    });
-  });
-
-  asyncTest("verifyEmailAddress with emailForVerficationToken XHR failure", function() {
-    xhr.useResult("ajaxError");
-    bid.verifyEmailAddress("token", function() {
-      testHelpers.testErrorVisible();
-      start();
-    });
-  });
-
-  asyncTest("submit with good token, both passwords", function() {
-    bid.verifyEmailAddress("token", function() {
-      $("#password").val("password");
-      $("#vpassword").val("password");
-
-      bid.verifyEmailAddress.submit(function() {
-        equal($("#congrats").is(":visible"), true, "congrats is visible, we are complete");
-        start();
-      });
-    });
-  });
-
-  asyncTest("submit with good token, missing password", function() {
-    bid.verifyEmailAddress("token", function() {
-      $("#password").val("");
-      $("#vpassword").val("password");
-
-      bid.verifyEmailAddress.submit(function() {
-        equal($("#congrats").is(":visible"), false, "congrats is not visible, missing password");
-        testTooltipVisible();
-        start();
-      });
-    });
-  });
-
-  asyncTest("submit with good token, too short of a password", function() {
-    bid.verifyEmailAddress("token", function() {
-      var pass = testHelpers.generateString(6);
-      $("#password").val(pass);
-      $("#vpassword").val(pass);
-
-      bid.verifyEmailAddress.submit(function() {
-        equal($("#congrats").is(":visible"), false, "congrats is not visible, too short of a password");
-        testTooltipVisible();
-        start();
-      });
-    });
-  });
-
-  asyncTest("submit with good token, too long of a password", function() {
-    bid.verifyEmailAddress("token", function() {
-      var pass = testHelpers.generateString(81);
-      $("#password").val(pass);
-      $("#vpassword").val(pass);
-
-      bid.verifyEmailAddress.submit(function() {
-        equal($("#congrats").is(":visible"), false, "congrats is not visible, too long of a password");
-        testTooltipVisible();
-        start();
-      });
-    });
-  });
-
-  asyncTest("submit with good token, missing verification password", function() {
-    bid.verifyEmailAddress("token");
-
-
-    $("#password").val("password");
-    $("#vpassword").val("");
-
-    bid.verifyEmailAddress.submit(function() {
-      equal($("#congrats").is(":visible"), false, "congrats is not visible, missing verification password");
-      testTooltipVisible();
-      start();
-    });
-
-  });
-
-  asyncTest("submit with good token, different passwords", function() {
-    bid.verifyEmailAddress("token");
-
-    $("#password").val("password");
-    $("#vpassword").val("pass");
-
-    bid.verifyEmailAddress.submit(function() {
-      equal($("#congrats").is(":visible"), false, "congrats is not visible, different passwords");
-      testTooltipVisible();
-      start();
-    });
-
-  });
-}());
diff --git a/resources/static/test/cases/pages/add_email_address_test.js b/resources/static/test/cases/pages/verify_secondary_address.js
similarity index 69%
rename from resources/static/test/cases/pages/add_email_address_test.js
rename to resources/static/test/cases/pages/verify_secondary_address.js
index 79fe35d5e22cb38c4888550cd30c91643f728d3d..a770237db60734b31e39a6d9f82553bb917b9bc5 100644
--- a/resources/static/test/cases/pages/add_email_address_test.js
+++ b/resources/static/test/cases/pages/verify_secondary_address.js
@@ -11,13 +11,15 @@
       xhr = bid.Mocks.xhr,
       dom = bid.DOM,
       testHelpers = bid.TestHelpers,
+      testHasClass = testHelpers.testHasClass,
       validToken = true,
       controller,
       config = {
-        token: "token"
+        token: "token",
+        verifyFunction: "verifyEmail"
       };
 
-  module("pages/add_email_address", {
+  module("pages/verify_secondary_address", {
     setup: function() {
       testHelpers.setup();
       bid.Renderer.render("#page_head", "site/add_email_address", {});
@@ -29,14 +31,14 @@
   });
 
   function createController(options, callback) {
-    controller = BrowserID.addEmailAddress.create();
+    controller = BrowserID.verifySecondaryAddress.create();
     options = options || {};
     options.ready = callback;
     controller.start(options);
   }
 
   function expectTooltipVisible() {
-    xhr.useResult("needsPassword");
+    xhr.useResult("mustAuth");
     createController(config, function() {
       controller.submit(function() {
         testHelpers.testTooltipVisible();
@@ -46,7 +48,7 @@
   }
 
   function testEmail() {
-    equal(dom.getInner(".email"), "testuser@testuser.com", "correct email shown");
+    equal(dom.getInner("#email"), "testuser@testuser.com", "correct email shown");
   }
 
   function testCannotConfirm() {
@@ -71,7 +73,7 @@
       testEmail();
       ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
       equal($(".website:nth(0)").text(), "browserid.org", "origin is updated");
-      equal($("body").hasClass("complete"), true, "body has complete class");
+      testHasClass("body", "complete");
       start();
     });
   });
@@ -102,59 +104,21 @@
     });
   });
 
-  asyncTest("password: first secondary address added", function() {
-    xhr.useResult("needsPassword");
-    createController(config, function() {
-      equal($("body").hasClass("enter_password"), true, "enter_password added to body");
-      testEmail();
-      start();
-    });
-  });
-
   asyncTest("password: missing password", function() {
     $("#password").val();
-    $("#vpassword").val("password");
-
-    expectTooltipVisible();
-  });
-
-  asyncTest("password: missing verify password", function() {
-    $("#password").val("password");
-    $("#vpassword").val();
-
-    expectTooltipVisible();
-  });
-
-  asyncTest("password: too short of a password", function() {
-    $("#password").val("pass");
-    $("#vpassword").val("pass");
-
-    expectTooltipVisible();
-  });
-
-  asyncTest("password: too long of a password", function() {
-    var tooLong = testHelpers.generateString(81);
-    $("#password").val(tooLong);
-    $("#vpassword").val(tooLong);
-
-    expectTooltipVisible();
-  });
-
-  asyncTest("password: mismatched passwords", function() {
-    $("#password").val("passwords");
-    $("#vpassword").val("password");
 
     expectTooltipVisible();
   });
 
   asyncTest("password: good password", function() {
     $("#password").val("password");
-    $("#vpassword").val("password");
 
+    xhr.useResult("mustAuth");
     createController(config, function() {
+      xhr.useResult("valid");
       controller.submit(function(status) {
         equal(status, true, "correct status");
-        equal($("body").hasClass("complete"), true, "body has complete class");
+        testHasClass("body", "complete");
         start();
       });
     });
@@ -162,7 +126,6 @@
 
   asyncTest("password: good password bad token", function() {
     $("#password").val("password");
-    $("#vpassword").val("password");
 
     xhr.useResult("invalid");
     createController(config, function() {
diff --git a/resources/static/test/cases/resources/helpers.js b/resources/static/test/cases/resources/helpers.js
index 279f7dee8576cfa7b24cb10fc43f6b678da5d73e..8ab3ed0a89d4127db4b6101a8a7aa2025328a103 100644
--- a/resources/static/test/cases/resources/helpers.js
+++ b/resources/static/test/cases/resources/helpers.js
@@ -21,7 +21,7 @@
       badError = testHelpers.unexpectedXHRFailure;
 
   var controllerMock = {
-    close: function(message, info) {
+    publish: function(message, info) {
       closeCB && closeCB(message, info);
     },
 
@@ -116,7 +116,7 @@
     xhr.useResult("unknown_secondary");
     closeCB = expectedClose("user_staged", "email", "unregistered@testuser.com");
 
-    dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
+    dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", "password", function(staged) {
       equal(staged, true, "user was staged");
       start();
     });
@@ -126,7 +126,7 @@
     closeCB = badClose;
 
     xhr.useResult("throttle");
-    dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
+    dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", "password", function(staged) {
       equal(staged, false, "user was not staged");
       start();
     });
@@ -136,7 +136,7 @@
     errorCB = expectedError;
 
     xhr.useResult("ajaxError");
-    dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", testHelpers.unexpectedSuccess);
+    dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", "password", testHelpers.unexpectedSuccess);
   });
 
   asyncTest("addEmail with primary email happy case, expects primary_user message", function() {
@@ -148,19 +148,11 @@
     });
   });
 
-  asyncTest("addEmail with unknown secondary email happy case", function() {
+  asyncTest("addEmail with secondary email - trigger add_email_submit_with_secondary", function() {
     xhr.useResult("unknown_secondary");
-    closeCB = expectedClose("email_staged", "email", "unregistered@testuser.com");
-    dialogHelpers.addEmail.call(controllerMock, "unregistered@testuser.com", function(status) {
-      ok(status, "correct status");
-      start();
-    });
-  });
-
-  asyncTest("addEmail throttled", function() {
-    xhr.useResult("throttle");
-    dialogHelpers.addEmail.call(controllerMock, "unregistered@testuser.com", function(added) {
-      equal(added, false, "email not added");
+    closeCB = expectedClose("add_email_submit_with_secondary", "email", "unregistered@testuser.com");
+    dialogHelpers.addEmail.call(controllerMock, "unregistered@testuser.com", function(success) {
+      equal(success, true, "success status");
       start();
     });
   });
@@ -185,8 +177,8 @@
   });
 
   asyncTest("resetPassword happy case", function() {
-    closeCB = expectedClose("reset_password", "email", "registered@testuser.com");
-    dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", function(reset) {
+    closeCB = expectedClose("password_reset", "email", "registered@testuser.com");
+    dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", "password", function(reset) {
       ok(reset, "password reset");
       start();
     });
@@ -195,7 +187,7 @@
 
   asyncTest("resetPassword throttled", function() {
     xhr.useResult("throttle");
-    dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", function(reset) {
+    dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", "password", function(reset) {
       equal(reset, false, "password not reset");
       start();
     });
@@ -205,7 +197,7 @@
     errorCB = expectedError;
 
     xhr.useResult("ajaxError");
-    dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", function(reset) {
+    dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", "password", function(reset) {
       ok(false, "unexpected close");
       start();
     });
diff --git a/resources/static/test/cases/resources/state.js b/resources/static/test/cases/resources/state.js
index 83e6c7cf6164e71f77b0befcd479e5a3754928ef..a8d859f1953e82c540e3723183dbc00dcd168b77 100644
--- a/resources/static/test/cases/resources/state.js
+++ b/resources/static/test/cases/resources/state.js
@@ -78,10 +78,44 @@
     equal(error, "start: controller must be specified", "creating a state machine without a controller fails");
   });
 
+  test("new_user - call doSetPassword with correct email", function() {
+    mediator.publish("new_user", { email: TEST_EMAIL });
+
+    equal(actions.info.doSetPassword.email, TEST_EMAIL, "correct email sent to doSetPassword");
+  });
+
+  test("cancel new user password_set flow - go back to the authentication screen", function() {
+    mediator.publish("authenticate");
+    mediator.publish("new_user", undefined, { email: TEST_EMAIL });
+    mediator.publish("password_set");
+    actions.info.doAuthenticate = {};
+    mediator.publish("cancel_state");
+    equal(actions.info.doAuthenticate.email, TEST_EMAIL, "authenticate called with the correct email");
+  });
+
+  test("password_set for new user - call doStageUser with correct email", function() {
+    mediator.publish("new_user", { email: TEST_EMAIL });
+    mediator.publish("password_set");
+
+    equal(actions.info.doStageUser.email, TEST_EMAIL, "correct email sent to doStageUser");
+  });
+
+  test("password_set for add secondary email - call doStageEmail with correct email", function() {
+    mediator.publish("add_email_submit_with_secondary", { email: TEST_EMAIL });
+    mediator.publish("password_set");
+
+    equal(actions.info.doStageEmail.email, TEST_EMAIL, "correct email sent to doStageEmail");
+  });
+
+  test("password_set for reset password - call doResetPassword 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");
+  });
+
   test("user_staged - call doConfirmUser", function() {
-    mediator.publish("user_staged", {
-      email: TEST_EMAIL
-    });
+    mediator.publish("user_staged", { email: TEST_EMAIL });
 
     equal(actions.info.doConfirmUser.email, TEST_EMAIL, "waiting for email confirmation for testuser@testuser.com");
   });
@@ -212,13 +246,13 @@
     equal(actions.info.doForgotPassword.requiredEmail, true, "correct requiredEmail passed");
   });
 
-  test("reset_password to user_confirmed - call doResetPassword then doEmailConfirmed", function() {
-    // reset_password indicates the user has verified that they want to reset
+  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("reset_password", {
+    mediator.publish("password_reset", {
       email: TEST_EMAIL
     });
-    equal(actions.info.doResetPassword.email, TEST_EMAIL, "reset password with the correct 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.
@@ -236,13 +270,13 @@
   });
 
 
-  test("cancel reset_password flow - go two steps back", function() {
+  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 reset_password flow.
+    // to the password_reset flow.
     mediator.publish("authenticate");
     mediator.publish("forgot_password", undefined, { email: TEST_EMAIL });
-    mediator.publish("reset_password", { 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");
@@ -451,4 +485,35 @@
     equal(actions.info.doPickEmail.tosURL, "http://example.com/tos.html", "tosURL preserved");
   });
 
+  test("add_email - call doAddEmail", function() {
+    mediator.publish("add_email", {
+      complete: function() {
+        equal(actions.called.doAddEmail, true, "doAddEmail called");
+        start();
+      }
+    });
+  });
+
+  test("add_email_submit_with_secondary - first secondary email - call doSetPassword", function() {
+    mediator.publish("add_email", {
+      complete: function() {
+        equal(actions.called.doSetPassword, true, "doSetPassword called");
+        start();
+      }
+    });
+  });
+
+
+  test("add_email_submit_with_secondary - second secondary email - call doStageEmail", function() {
+    storage.addSecondaryEmail("testuser@testuser.com");
+
+    mediator.publish("add_email", {
+      complete: function() {
+        equal(actions.called.doStageEmail, true, "doStageEmail called");
+        start();
+      }
+    });
+  });
+
+
 }());
diff --git a/resources/static/test/cases/shared/helpers.js b/resources/static/test/cases/shared/helpers.js
index 8ebdd3f7dd92c4d01e0ad85ad3785759df892254..c29b406421024f1d23dfbc8ba8b0ab994b4c9a37 100644
--- a/resources/static/test/cases/shared/helpers.js
+++ b/resources/static/test/cases/shared/helpers.js
@@ -13,7 +13,7 @@
   module("shared/helpers", {
     setup: function() {
       testHelpers.setup();
-      bid.Renderer.render("#page_head", "site/add_email_address", {});
+      bid.Renderer.render("#page_head", "site/signin", {});
     },
 
     teardown: function() {
diff --git a/resources/static/test/cases/shared/network.js b/resources/static/test/cases/shared/network.js
index 1fc02f083dcd60aa6bec431d9312f8faed97066e..e98a049029126e6eb623820d7a95fae563f7a7ea 100644
--- a/resources/static/test/cases/shared/network.js
+++ b/resources/static/test/cases/shared/network.js
@@ -11,7 +11,9 @@
       transport = bid.Mocks.xhr,
       testHelpers = bid.TestHelpers,
       TEST_EMAIL = "testuser@testuser.com",
-      failureCheck = testHelpers.failureCheck;
+      TEST_PASSWORD = "password",
+      failureCheck = testHelpers.failureCheck,
+      testObjectValuesEqual = testHelpers.testObjectValuesEqual;
 
   var network = BrowserID.Network;
 
@@ -172,6 +174,13 @@
     }, testHelpers.unexpectedXHRFailure);
   });
 
+  asyncTest("completeEmailRegistration with valid token, missing password", function() {
+    transport.useResult("missing_password");
+    network.completeEmailRegistration("token", undefined,
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectedXHRFailure);
+  });
+
   asyncTest("completeEmailRegistration with invalid token", function() {
     transport.useResult("invalid");
     network.completeEmailRegistration("badtoken", "password", function onSuccess(proven) {
@@ -185,7 +194,7 @@
   });
 
   asyncTest("createUser with valid user", function() {
-    network.createUser("validuser", "origin", function onSuccess(created) {
+    network.createUser("validuser", "password", "origin", function onSuccess(created) {
       ok(created);
       start();
     }, testHelpers.unexpectedFailure);
@@ -193,7 +202,7 @@
 
   asyncTest("createUser with invalid user", function() {
     transport.useResult("invalid");
-    network.createUser("invaliduser", "origin", function onSuccess(created) {
+    network.createUser("invaliduser", "password", "origin", function onSuccess(created) {
       equal(created, false);
       start();
     }, testHelpers.unexpectedFailure);
@@ -202,14 +211,14 @@
   asyncTest("createUser throttled", function() {
     transport.useResult("throttle");
 
-    network.createUser("validuser", "origin", function onSuccess(added) {
+    network.createUser("validuser", "password", "origin", function onSuccess(added) {
       equal(added, false, "throttled email returns onSuccess but with false as the value");
       start();
     }, testHelpers.unexpectedFailure);
   });
 
   asyncTest("createUser with XHR failure", function() {
-    failureCheck(network.createUser, "validuser", "origin");
+    failureCheck(network.createUser, "validuser", "password", "origin");
   });
 
   asyncTest("checkUserRegistration returns pending - pending status, user is not logged in", function() {
@@ -264,7 +273,21 @@
     failureCheck(network.checkUserRegistration, "registered@testuser.com");
   });
 
-  asyncTest("completeUserRegistration with valid token", function() {
+  asyncTest("completeUserRegistration with valid token, no password required", function() {
+    network.completeUserRegistration("token", undefined, function(registered) {
+      ok(registered);
+      start();
+    }, testHelpers.unexpectedFailure);
+  });
+
+  asyncTest("completeUserRegistration with valid token, missing password", function() {
+    transport.useResult("missing_password");
+    network.completeUserRegistration("token", undefined,
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectedXHRFailure);
+  });
+
+  asyncTest("completeUserRegistration with valid token, password required", function() {
     network.completeUserRegistration("token", "password", function(registered) {
       ok(registered);
       start();
@@ -327,7 +350,7 @@
 
 
   asyncTest("addSecondaryEmail valid", function() {
-    network.addSecondaryEmail("address", "origin", function onSuccess(added) {
+    network.addSecondaryEmail(TEST_EMAIL, TEST_PASSWORD, "origin", function onSuccess(added) {
       ok(added);
       start();
     }, testHelpers.unexpectedFailure);
@@ -335,7 +358,7 @@
 
   asyncTest("addSecondaryEmail invalid", function() {
     transport.useResult("invalid");
-    network.addSecondaryEmail("address", "origin", function onSuccess(added) {
+    network.addSecondaryEmail(TEST_EMAIL, TEST_PASSWORD, "origin", function onSuccess(added) {
       equal(added, false);
       start();
     }, testHelpers.unexpectedFailure);
@@ -344,14 +367,14 @@
   asyncTest("addSecondaryEmail throttled", function() {
     transport.useResult("throttle");
 
-    network.addSecondaryEmail("address", "origin", function onSuccess(added) {
+    network.addSecondaryEmail(TEST_EMAIL, TEST_PASSWORD, "origin", function onSuccess(added) {
       equal(added, false, "throttled email returns onSuccess but with false as the value");
       start();
     }, testHelpers.unexpectedFailure);
   });
 
   asyncTest("addSecondaryEmail with XHR failure", function() {
-    failureCheck(network.addSecondaryEmail, "address", "origin");
+    failureCheck(network.addSecondaryEmail, TEST_EMAIL, TEST_PASSWORD, "origin");
   });
 
   asyncTest("checkEmailRegistration pending", function() {
@@ -377,7 +400,7 @@
   });
 
   asyncTest("checkEmailRegistration with XHR failure", function() {
-    failureCheck(network.checkEmailRegistration, "address");
+    failureCheck(network.checkEmailRegistration, TEST_EMAIL);
   });
 
 
@@ -415,12 +438,11 @@
     }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("emailForVerificationToken that needs password - returns needs_password and email address", function() {
-    transport.useResult("needsPassword");
+  asyncTest("emailForVerificationToken that must authenticate - returns must_auth and email address", function() {
+    transport.useResult("mustAuth");
 
     network.emailForVerificationToken("token", function(result) {
-      equal(result.needs_password, true, "needs_password correctly set to true");
-      equal(result.email, "testuser@testuser.com", "email address correctly added");
+      testObjectValuesEqual(result, { must_auth: true, email: TEST_EMAIL });
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
@@ -428,7 +450,7 @@
   asyncTest("emailForVerificationToken that does not need password", function() {
     network.emailForVerificationToken("token", function(result) {
       equal(result.needs_password, false, "needs_password correctly set to false");
-      equal(result.email, "testuser@testuser.com", "email address correctly added");
+      equal(result.email, TEST_EMAIL, "email address correctly added");
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
@@ -456,16 +478,15 @@
   });
 
 
-  asyncTest("requestPasswordReset", function() {
-    network.requestPasswordReset("address", "origin", function onSuccess() {
-      // XXX need a test here;
-      ok(true);
+  asyncTest("requestPasswordReset - true status", function() {
+    network.requestPasswordReset(TEST_EMAIL, "password", "origin", function onSuccess(status) {
+      equal(status, true, "password reset request success");
       start();
     }, testHelpers.unexpectedFailure);
   });
 
   asyncTest("requestPasswordReset with XHR failure", function() {
-    failureCheck(network.requestPasswordReset, "address", "origin");
+    failureCheck(network.requestPasswordReset, TEST_EMAIL, "password", "origin");
   });
 
   asyncTest("setPassword happy case expects true status", function() {
@@ -596,7 +617,7 @@
   });
 
   asyncTest("prolongSession with authenticated user, success - call complete", function() {
-    network.authenticate("testuser@testuser.com", "password", function() {
+    network.authenticate(TEST_EMAIL, "password", function() {
       network.prolongSession(function() {
         ok(true, "prolongSession completed");
         start();
diff --git a/resources/static/test/cases/shared/storage.js b/resources/static/test/cases/shared/storage.js
index c6f459e6264d103e8fc589b9479047dc287e0fb6..b9b230b5e362aacd4174960d173810eabea5faa7 100644
--- a/resources/static/test/cases/shared/storage.js
+++ b/resources/static/test/cases/shared/storage.js
@@ -34,6 +34,20 @@
     equal("key", id.priv, "email that was added is retrieved");
   });
 
+  test("addPrimaryEmail", function() {
+    storage.addPrimaryEmail("testuser@testuser.com");
+
+    var email = storage.getEmail("testuser@testuser.com");
+    equal(email.type, "primary", "email type set correctly");
+  });
+
+  test("addSecondaryEmail", function() {
+    storage.addSecondaryEmail("testuser@testuser.com");
+
+    var email = storage.getEmail("testuser@testuser.com");
+    equal(email.type, "secondary", "email type set correctly");
+  });
+
   test("removeEmail, getEmails", function() {
     storage.addEmail("testuser@testuser.com", {priv: "key"});
     storage.removeEmail("testuser@testuser.com");
diff --git a/resources/static/test/cases/shared/user.js b/resources/static/test/cases/shared/user.js
index a1269fb9305ca913aa41344474b19a4f1f3b52a8..455736004a20fc4b675c16c70b135ba6d6b5fc01 100644
--- a/resources/static/test/cases/shared/user.js
+++ b/resources/static/test/cases/shared/user.js
@@ -132,53 +132,32 @@ var vep = require("./vep");
     equal(0, count, "after clearing, there are no identities");
   });
 
-  asyncTest("createSecondaryUser", function() {
-    lib.createSecondaryUser(TEST_EMAIL, function(status) {
+  asyncTest("createSecondaryUser success - callback with true status", function() {
+    lib.createSecondaryUser(TEST_EMAIL, "password", function(status) {
       ok(status, "user created");
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("createSecondaryUser with user creation refused", function() {
+  asyncTest("createSecondaryUser throttled - callback with false status", function() {
     xhr.useResult("throttle");
 
-    lib.createSecondaryUser(TEST_EMAIL, function(status) {
+    lib.createSecondaryUser(TEST_EMAIL, "password", function(status) {
       equal(status, false, "user creation refused");
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
 
   asyncTest("createSecondaryUser with XHR failure", function() {
-    failureCheck(lib.createSecondaryUser, TEST_EMAIL);
+    failureCheck(lib.createSecondaryUser, TEST_EMAIL, "password");
   });
 
-  asyncTest("createUser with unknown secondary happy case - expect 'secondary.verify'", function() {
-    xhr.useResult("unknown_secondary");
-
-    lib.createUser("unregistered@testuser.com", function(status) {
-      equal(status, "secondary.verify", "secondary user must be verified");
-      start();
-    }, testHelpers.unexpectedXHRFailure);
-  });
-
-  asyncTest("createUser with unknown secondary, throttled - expect status='secondary.could_not_add'", function() {
-    xhr.useResult("throttle");
 
-    lib.createUser("unregistered@testuser.com", function(status) {
-      equal(status, "secondary.could_not_add", "user creation refused");
-      start();
-    }, testHelpers.unexpectedXHRFailure);
-  });
-
-  asyncTest("createUser with unknown secondary, XHR failure - expect failure call", function() {
-    failureCheck(lib.createUser, "unregistered@testuser.com");
-  });
-
-  asyncTest("createUser with primary, user verified with primary - expect 'primary.verified'", function() {
+  asyncTest("createPrimaryUser with primary, user verified with primary - expect 'primary.verified'", function() {
     xhr.useResult("primary");
     provisioning.setStatus(provisioning.AUTHENTICATED);
 
-    lib.createUser("unregistered@testuser.com", function(status) {
+    lib.createPrimaryUser({email: "unregistered@testuser.com"}, function(status) {
       equal(status, "primary.verified", "primary user is already verified, correct status");
       network.checkAuth(function(authenticated) {
         equal(authenticated, "assertion", "after provisioning user, user should be automatically authenticated to BrowserID");
@@ -187,33 +166,28 @@ var vep = require("./vep");
     }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("createUser with primary, user must authenticate with primary - expect 'primary.verify'", function() {
+  asyncTest("createPrimaryUser with primary, user must authenticate with primary - expect 'primary.verify'", function() {
     xhr.useResult("primary");
 
-    lib.createUser("unregistered@testuser.com", function(status) {
+    lib.createPrimaryUser({email: "unregistered@testuser.com"}, function(status) {
       equal(status, "primary.verify", "primary must verify with primary, correct status");
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("createUser with primary, unknown provisioning failure, expect XHR failure callback", function() {
+  asyncTest("createPrimaryUser with primary, unknown provisioning failure, expect XHR failure callback", function() {
     xhr.useResult("primary");
     provisioning.setFailure({
       code: "primaryError",
       msg: "some error"
     });
 
-    lib.createUser("unregistered@testuser.com",
+    lib.createPrimaryUser({email: "unregistered@testuser.com"},
       testHelpers.unexpectedSuccess,
       testHelpers.expectedXHRFailure
     );
   });
 
-  asyncTest("createUserWithInfo", function() {
-    ok(true, "For development speed and reduced duplication of tests, tested via createUser");
-    start();
-  });
-
   asyncTest("provisionPrimaryUser authenticated with IdP, expect primary.verified", function() {
     xhr.useResult("primary");
     provisioning.setStatus(provisioning.AUTHENTICATED);
@@ -489,24 +463,24 @@ var vep = require("./vep");
     );
   });
 
-  asyncTest("requestPasswordReset with known email", function() {
-    lib.requestPasswordReset("registered@testuser.com", function(status) {
+  asyncTest("requestPasswordReset with known email - true status", function() {
+    lib.requestPasswordReset("registered@testuser.com", "password", function(status) {
       equal(status.success, true, "password reset for known user");
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("requestPasswordReset with unknown email", function() {
-    lib.requestPasswordReset("unregistered@testuser.com", function(status) {
+  asyncTest("requestPasswordReset with unknown email - false status, invalid_user", function() {
+    lib.requestPasswordReset("unregistered@testuser.com", "password", function(status) {
       equal(status.success, false, "password not reset for unknown user");
       equal(status.reason, "invalid_user", "invalid_user is the reason");
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("requestPasswordReset with throttle", function() {
+  asyncTest("requestPasswordReset with throttle - false status, throttle", function() {
     xhr.useResult("throttle");
-    lib.requestPasswordReset("registered@testuser.com", function(status) {
+    lib.requestPasswordReset("registered@testuser.com", "password", function(status) {
       equal(status.success, false, "password not reset for throttle");
       equal(status.reason, "throttle", "password reset was throttled");
       start();
@@ -514,7 +488,7 @@ var vep = require("./vep");
   });
 
   asyncTest("requestPasswordReset with XHR failure", function() {
-    failureCheck(lib.requestPasswordReset, "registered@testuser.com");
+    failureCheck(lib.requestPasswordReset, "registered@testuser.com", "password");
   });
 
 
@@ -614,8 +588,36 @@ var vep = require("./vep");
     failureCheck(lib.isEmailRegistered, "registered");
   });
 
+  asyncTest("passwordNeededToAddSecondaryEmail, account only has primaries - call callback with true", function() {
+    storage.addEmail("testuser@testuser.com", { type: "primary" });
+
+    lib.passwordNeededToAddSecondaryEmail(function(passwordNeeded) {
+      equal(passwordNeeded, true, "password correctly needed");
+      start();
+    });
+  });
+
+  asyncTest("passwordNeededToAddSecondaryEmail, account already has secondary - call callback with false", function() {
+    storage.addEmail("testuser@testuser.com", { type: "secondary" });
+
+    lib.passwordNeededToAddSecondaryEmail(function(passwordNeeded) {
+      equal(passwordNeeded, false, "password not needed");
+      start();
+    });
+  });
+
+  asyncTest("passwordNeededToAddSecondaryEmail, mix of types - call callback with false", function() {
+    storage.addEmail("testuser@testuser.com", { type: "primary" });
+    storage.addEmail("testuser1@testuser.com", { type: "secondary" });
+
+    lib.passwordNeededToAddSecondaryEmail(function(passwordNeeded) {
+      equal(passwordNeeded, false, "password not needed");
+      start();
+    });
+  });
+
   asyncTest("addEmail", function() {
-    lib.addEmail("testemail@testemail.com", function(added) {
+    lib.addEmail("testemail@testemail.com", "password", function(added) {
       ok(added, "user was added");
 
       var identities = lib.getStoredEmailKeypairs();
@@ -630,7 +632,7 @@ var vep = require("./vep");
   asyncTest("addEmail with addition refused", function() {
     xhr.useResult("throttle");
 
-    lib.addEmail("testemail@testemail.com", function(added) {
+    lib.addEmail("testemail@testemail.com", "password", function(added) {
       equal(added, false, "user addition was refused");
 
       var identities = lib.getStoredEmailKeypairs();
@@ -643,7 +645,7 @@ var vep = require("./vep");
   });
 
   asyncTest("addEmail with XHR failure", function() {
-    failureCheck(lib.addEmail, "testemail@testemail.com");
+    failureCheck(lib.addEmail, "testemail@testemail.com", "password");
   });
 
 
@@ -713,42 +715,9 @@ var vep = require("./vep");
     }, 500);
   });
 
-  asyncTest("verifyEmailNoPassword with a good token - callback with email, orgiin, and valid", function() {
-    storage.setStagedOnBehalfOf(testOrigin);
-    lib.verifyEmailNoPassword("token", function onSuccess(info) {
-
-      ok(info.valid, "token was valid");
-      equal(info.email, TEST_EMAIL, "email part of info");
-      equal(info.origin, testOrigin, "origin in info");
-      equal(storage.getStagedOnBehalfOf(), "", "initiating origin was removed");
-
-      start();
-    }, testHelpers.unexpectedXHRFailure);
-  });
-
-  asyncTest("verifyEmailNoPassword with a bad token - callback with valid: false", function() {
-    xhr.useResult("invalid");
-
-    lib.verifyEmailNoPassword("token", function onSuccess(info) {
-      equal(info.valid, false, "bad token calls onSuccess with a false validity");
-
-      start();
-    }, testHelpers.unexpectedXHRFailure);
-  });
-
-  asyncTest("verifyEmailNoPassword with an XHR failure", function() {
-    xhr.useResult("ajaxError");
-
-    lib.verifyEmailNoPassword(
-      "token",
-      testHelpers.unexpectedSuccess,
-      testHelpers.expectedXHRFailure
-    );
-  });
-
-  asyncTest("verifyEmailWithPassword with a good token - callback with email, origin, valid", function() {
+  asyncTest("verifyEmail with a good token - callback with email, origin, valid", function() {
     storage.setStagedOnBehalfOf(testOrigin);
-    lib.verifyEmailWithPassword("token", "password", function onSuccess(info) {
+    lib.verifyEmail("token", "password", function onSuccess(info) {
 
       ok(info.valid, "token was valid");
       equal(info.email, TEST_EMAIL, "email part of info");
@@ -759,20 +728,20 @@ var vep = require("./vep");
     }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("verifyEmailWithPassword with a bad token - callback with valid: false", function() {
+  asyncTest("verifyEmail with a bad token - callback with valid: false", function() {
     xhr.useResult("invalid");
 
-    lib.verifyEmailWithPassword("token", "password", function onSuccess(info) {
+    lib.verifyEmail("token", "password", function onSuccess(info) {
       equal(info.valid, false, "bad token calls onSuccess with a false validity");
 
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("verifyEmailWithPassword with an XHR failure", function() {
+  asyncTest("verifyEmail with an XHR failure", function() {
     xhr.useResult("ajaxError");
 
-    lib.verifyEmailWithPassword(
+    lib.verifyEmail(
       "token",
       "password",
       testHelpers.unexpectedSuccess,
diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js
index d9048e7bfd80a82e99b6b6f8525c8c0e1c2f7630..8b505f997d4680903d922e8de6e2a90d78f08cc1 100644
--- a/resources/static/test/mocks/xhr.js
+++ b/resources/static/test/mocks/xhr.js
@@ -33,7 +33,7 @@ BrowserID.Mocks.xhr = (function() {
       // the flag contextAjaxError.
       "get /wsapi/session_context contextAjaxError": undefined,
       "get /wsapi/email_for_token?token=token valid": { email: "testuser@testuser.com" },
-      "get /wsapi/email_for_token?token=token needsPassword": { email: "testuser@testuser.com", needs_password: true },
+      "get /wsapi/email_for_token?token=token mustAuth": { email: "testuser@testuser.com", must_auth: true },
       "get /wsapi/email_for_token?token=token invalid": { success: false },
       "post /wsapi/authenticate_user valid": { success: true, userid: 1 },
       "post /wsapi/authenticate_user invalid": { success: false },
@@ -47,6 +47,7 @@ BrowserID.Mocks.xhr = (function() {
       "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 missing_password": 401,
       "post /wsapi/complete_email_addition invalid": { success: false },
       "post /wsapi/complete_email_addition ajaxError": undefined,
       "post /wsapi/stage_user unknown_secondary": { success: true },
@@ -60,6 +61,7 @@ BrowserID.Mocks.xhr = (function() {
       "get /wsapi/user_creation_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" },
       "get /wsapi/user_creation_status?email=registered%40testuser.com ajaxError": undefined,
       "post /wsapi/complete_user_creation valid": { success: true },
+      "post /wsapi/complete_user_creation missing_password": 401,
       "post /wsapi/complete_user_creation invalid": { success: false },
       "post /wsapi/complete_user_creation ajaxError": undefined,
       "post /wsapi/logout valid": { success: true },
@@ -69,6 +71,7 @@ BrowserID.Mocks.xhr = (function() {
       "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=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 },
       "post /wsapi/remove_email invalid": { success: false },
       "post /wsapi/remove_email multiple": { success: true },
@@ -105,6 +108,7 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/update_password invalid": undefined,
       "get /wsapi/address_info?email=unregistered%40testuser.com invalid": undefined,
       "get /wsapi/address_info?email=unregistered%40testuser.com throttle": { type: "secondary", known: false },
+      "get /wsapi/address_info?email=unregistered%40testuser.com valid": { type: "secondary", known: false },
       "get /wsapi/address_info?email=unregistered%40testuser.com unknown_secondary": { type: "secondary", known: false },
       "get /wsapi/address_info?email=registered%40testuser.com known_secondary": { type: "secondary", known: true },
       "get /wsapi/address_info?email=registered%40testuser.com primary": { type: "primary", auth: "https://auth_url", prov: "https://prov_url" },
diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js
index 910db36bd27f94bb2265896c3c2e103077c7a510..287ba8393c039d0519f9c892fcc79495f62d051f 100644
--- a/resources/static/test/testHelpers/helpers.js
+++ b/resources/static/test/testHelpers/helpers.js
@@ -29,7 +29,7 @@ BrowserID.TestHelpers = (function() {
       }
       calls[msg] = true;
 
-      cb && cb(msg, info);
+      cb && cb.apply(null, arguments);
     }));
   }
 
@@ -203,6 +203,17 @@ BrowserID.TestHelpers = (function() {
       for(var i=0, key; key=expected[i]; ++i) {
         ok(key in objToTest, msg || ("object contains " + key));
       }
+    },
+
+    testObjectValuesEqual: function(objToTest, expected, msg) {
+      for(var key in expected) {
+        equal(objToTest[key], expected[key], key + " set to: " + expected[key] + (msg ? " - " + msg : ""));
+      }
+    },
+
+    testHasClass: function(selector, className, msg) {
+      ok($(selector).hasClass(className),
+          selector + " has className " + className + " - " + msg);
     }
 
   };
diff --git a/resources/views/add_email_address.ejs b/resources/views/add_email_address.ejs
index 979cea68aa1953636faecadd5c2370d81a366722..fa6fbd891b20f14c23532b9497da17adf3127e16 100644
--- a/resources/views/add_email_address.ejs
+++ b/resources/views/add_email_address.ejs
@@ -14,14 +14,14 @@
 
             <h1 class="serif"><%= gettext('Email Verification') %></h1>
 
-            <ul class="inputs password_entry">
+            <ul class="inputs">
                 <li>
                     <label class="serif" for="email"><%= gettext('Email Address') %></label>
-                    <input class="youraddress sans email" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" />
+                    <input class="youraddress sans" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" />
                 </li>
-                <li>
-                    <label class="serif" for="password"><%= gettext('New Password') %></label>
-                    <input class="sans" id="password" placeholder="<%= gettext('Enter a Password') %>" type="password" autofocus maxlength=80 />
+                <li class="password_entry">
+                    <label class="serif" for="password"><%= gettext('Password') %></label>
+                    <input class="sans" id="password" placeholder="<%= gettext('Your Password') %>" type="password" autofocus maxlength=80 />
 
                     <div class="tooltip" id="password_required" for="password">
                       <%= gettext('Password is required.') %>
@@ -31,18 +31,6 @@
                       <%= gettext('Password must be between 8 and 80 characters long.') %>
                     </div>
                 </li>
-                <li>
-                    <label class="serif" for="vpassword"><%= gettext('Verify Password') %></label>
-                    <input class="sans" id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength=80 />
-
-                    <div class="tooltip" id="vpassword_required" for="vpassword">
-                      <%= gettext('Verification password is required.') %>
-                    </div>
-
-                    <div class="tooltip" id="passwords_no_match" for="vpassword">
-                      <%= gettext('Passwords do not match.') %>
-                    </div>
-                </li>
             </ul>
 
             <div class="submit cf password_entry">
diff --git a/resources/views/forgot.ejs b/resources/views/forgot.ejs
index f249dc610c694a19aaf15ed8c894aab0b6cad483..fea0f0c37a75d4f66421a65586bebf887bb0756c 100644
--- a/resources/views/forgot.ejs
+++ b/resources/views/forgot.ejs
@@ -46,6 +46,36 @@
                     </div>
                 </li>
 
+                <li>
+                    <label class="serif" for="password">Password</label>
+                    <input class="sans" id="password" placeholder="Password" type="password" maxlength="80">
+
+                    <div id="password_required" class="tooltip" for="password">
+                        Password is required.
+                    </div>
+
+                    <div class="tooltip" id="password_length" for="password">
+                        Password must be between 8 and 80 characters long.
+                    </div>
+
+                    <div id="could_not_add" class="tooltip" for="password">
+                        We just sent an email to that address! If you really want to send another, wait a minute or two and try again.
+                    </div>
+                </li>
+
+                <li>
+                    <label class="serif" for="vpassword">Verify Password</label>
+                    <input class="sans" id="vpassword" placeholder="Repeat Password" type="password" maxlength="80">
+
+                    <div id="password_required" class="tooltip" for="vpassword">
+                      Verification password is required.
+                    </div>
+
+                    <div class="tooltip" id="passwords_no_match" for="vpassword">
+                      Passwords do not match.
+                    </div>
+
+                </li>
 
             </ul>
 
diff --git a/resources/views/signup.ejs b/resources/views/signup.ejs
index 51b280608d0900893cf11ab0a38532e94bd25328..6e786017f8f383c8af4415c7b49de23fb741a312 100644
--- a/resources/views/signup.ejs
+++ b/resources/views/signup.ejs
@@ -50,6 +50,37 @@
                       We just sent an email to that address! If you really want to send another, wait a minute or two and try again.
                     </div>
                 </li>
+
+                <li class="password_entry">
+                    <label class="serif" for="password">Password</label>
+                    <input class="sans" id="password" placeholder="Password" type="password" tabindex="2" maxlength="80">
+
+                    <div id="password_required" class="tooltip" for="password">
+                        Password is required.
+                    </div>
+
+                    <div class="tooltip" id="password_length" for="password">
+                        Password must be between 8 and 80 characters long.
+                    </div>
+
+                    <div id="could_not_add" class="tooltip" for="password">
+                        We just sent an email to that address! If you really want to send another, wait a minute or two and try again.
+                    </div>
+                </li>
+
+                <li class="password_entry">
+                    <label class="serif" for="vpassword">Verify Password</label>
+                    <input class="sans" id="vpassword" placeholder="Repeat Password" type="password" tabindex="2" maxlength="80">
+
+                    <div id="password_required" class="tooltip" for="vpassword">
+                      Verification password is required.
+                    </div>
+
+                    <div class="tooltip" id="passwords_no_match" for="vpassword">
+                      Passwords do not match.
+                    </div>
+
+                </li>
             </ul>
 
             <div class="submit cf forminputs">
diff --git a/resources/views/test.ejs b/resources/views/test.ejs
index 3d55b817e19316d0226c033ec9dbec85ddc13020..4e0e1960d5644540dbf57728692c58e0b0e1f22d 100644
--- a/resources/views/test.ejs
+++ b/resources/views/test.ejs
@@ -120,21 +120,20 @@
     <script src="/dialog/controllers/dialog.js"></script>
     <script src="/dialog/controllers/check_registration.js"></script>
     <script src="/dialog/controllers/authenticate.js"></script>
-    <script src="/dialog/controllers/forgot_password.js"></script>
     <script src="/dialog/controllers/required_email.js"></script>
     <script src="/dialog/controllers/verify_primary_user.js"></script>
     <script src="/dialog/controllers/generate_assertion.js"></script>
     <script src="/dialog/controllers/provision_primary_user.js"></script>
     <script src="/dialog/controllers/primary_user_provisioned.js"></script>
     <script src="/dialog/controllers/is_this_your_computer.js"></script>
+    <script src="/dialog/controllers/set_password.js"></script>
 
     <script src="/pages/page_helpers.js"></script>
-    <script src="/pages/add_email_address.js"></script>
+    <script src="/pages/verify_secondary_address.js"></script>
     <script src="/pages/forgot.js"></script>
     <script src="/pages/manage_account.js"></script>
     <script src="/pages/signin.js"></script>
     <script src="/pages/signup.js"></script>
-    <script src="/pages/verify_email_address.js"></script>
 
     <script src="testHelpers/helpers.js"></script>
 
@@ -164,8 +163,7 @@
 
     <script src="cases/pages/browserid.js"></script>
     <script src="cases/pages/page_helpers.js"></script>
-    <script src="cases/pages/add_email_address_test.js"></script>
-    <script src="cases/pages/verify_email_address_test.js"></script>
+    <script src="cases/pages/verify_secondary_address.js"></script>
     <script src="cases/pages/forgot.js"></script>
     <script src="cases/pages/signin.js"></script>
     <script src="cases/pages/signup.js"></script>
@@ -180,13 +178,13 @@
     <script src="cases/controllers/add_email.js"></script>
     <script src="cases/controllers/check_registration.js"></script>
     <script src="cases/controllers/authenticate.js"></script>
-    <script src="cases/controllers/forgot_password.js"></script>
     <script src="cases/controllers/required_email.js"></script>
     <script src="cases/controllers/verify_primary_user.js"></script>
     <script src="cases/controllers/generate_assertion.js"></script>
     <script src="cases/controllers/provision_primary_user.js"></script>
     <script src="cases/controllers/primary_user_provisioned.js"></script>
     <script src="cases/controllers/is_this_your_computer.js"></script>
+    <script src="cases/controllers/set_password.js"></script>
 
     <!-- must go last or all other tests will fail. -->
     <script src="cases/controllers/dialog.js"></script>
diff --git a/resources/views/verify_email_address.ejs b/resources/views/verify_email_address.ejs
index e86a1f5b6a41afa004449cae41925c9636ca6104..a73c80ead95230830d13bc8f218e5e84e878c314 100644
--- a/resources/views/verify_email_address.ejs
+++ b/resources/views/verify_email_address.ejs
@@ -6,7 +6,6 @@
     <div id="signUpFormWrap">
         <ul class="notifications">
             <li class="notification error" id="cannotconfirm"><%= gettext('There was a problem with your signup link. Has this address already been registered?') %></li>
-            <li class="notification error" id="cannotcommunicate"><%= gettext('Error communicating with server.') %></li>
             <li class="notification error" id="cannotcomplete"><%= gettext('Error encountered trying to complete registration.') %></li>
         </ul>
 
@@ -19,9 +18,9 @@
                     <label class="serif" for="email"><%= gettext('Email Address') %></label>
                     <input class="youraddress sans" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" />
                 </li>
-                <li>
-                    <label class="serif" for="password"><%= gettext('New Password') %></label>
-                    <input class="sans" id="password" placeholder="<%= gettext('Enter a Password') %>" type="password" autofocus maxlength=80 />
+                <li class="password_entry">
+                    <label class="serif" for="password"><%= gettext('Password') %></label>
+                    <input class="sans" id="password" placeholder="<%= gettext('Your Password') %>" type="password" autofocus maxlength=80 />
 
                     <div class="tooltip" id="password_required" for="password">
                       <%= gettext('Password is required.') %>
@@ -31,21 +30,9 @@
                       <%= gettext('Password must be between 8 and 80 characters long.') %>
                     </div>
                 </li>
-                <li>
-                    <label class="serif" for="vpassword"><%= gettext('Verify Password') %></label>
-                    <input class="sans" id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength=80 />
-
-                    <div class="tooltip" id="vpassword_required" for="vpassword">
-                      <%= gettext('Verification password is required.') %>
-                    </div>
-
-                    <div class="tooltip" id="passwords_no_match" for="vpassword">
-                      <%= gettext('Passwords do not match.') %>
-                    </div>
-                </li>
             </ul>
 
-            <div class="submit cf">
+            <div class="submit cf password_entry">
                 <button><%= gettext('finish') %></button>
             </div>
 
diff --git a/scripts/deploy/vm.js b/scripts/deploy/vm.js
index 5508d1b790e98e7af3354a90304f8bba6864f020..c637fd27abcc4039616ee2e7f9d6145c3af7a8a1 100644
--- a/scripts/deploy/vm.js
+++ b/scripts/deploy/vm.js
@@ -4,7 +4,7 @@ jsel = require('JSONSelect'),
 key = require('./key.js'),
 sec = require('./sec.js');
 
-const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-c38f57aa';
+const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-c2a401ab';
 
 function extractInstanceDeets(horribleBlob) {
   var instance = {};
diff --git a/tests/add-email-with-assertion-test.js b/tests/add-email-with-assertion-test.js
index aca86c04c874d112f634a5a0b13d8e1bbb992dbb..19a58f0ae405b8c595101759dea3e47c14d5997e 100755
--- a/tests/add-email-with-assertion-test.js
+++ b/tests/add-email-with-assertion-test.js
@@ -112,6 +112,7 @@ suite.addBatch({
   "stage an account": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: TEST_FIRST_ACCT,
+      pass: 'fakepass',
       site:'http://fakesite.com:652'
     }),
     "works": function(err, r) {
@@ -126,7 +127,7 @@ suite.addBatch({
       },
       "can be used": {
         topic: function(token) {
-          wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'fakepass' }).call(this);
+          wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
         },
         "to verify email ownership": function(err, r) {
           assert.equal(r.code, 200);
diff --git a/tests/cert-emails-test.js b/tests/cert-emails-test.js
index 3bfba17e5ef1e0ffe4d14fdca5acd4a66c151937..a8f746f568d1fd78242098618fe9c393984a77e2 100755
--- a/tests/cert-emails-test.js
+++ b/tests/cert-emails-test.js
@@ -32,7 +32,7 @@ suite.addBatch({
   "staging an account": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'syncer@somehost.com',
-      pubkey: 'fakekey',
+      pass: 'fakepass',
       site:'http://fakesite.com'
     }),
     "succeeds": function(err, r) {
@@ -57,7 +57,7 @@ suite.addBatch({
 suite.addBatch({
   "verifying account ownership": {
     topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'fakepass' }).call(this);
+      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
     },
     "works": function(err, r) {
       assert.equal(r.code, 200);
diff --git a/tests/db-test.js b/tests/db-test.js
index 65abaffa2926f260fd0157a7ee27dbf9f36d5565..6c8ec1ea54b157fb05f29e74918049d7fd4cead2 100755
--- a/tests/db-test.js
+++ b/tests/db-test.js
@@ -73,7 +73,7 @@ suite.addBatch({
 suite.addBatch({
   "stage a user for creation pending verification": {
     topic: function() {
-      db.stageUser('lloyd@nowhe.re', this.callback);
+      db.stageUser('lloyd@nowhe.re', 'biglonghashofapassword', this.callback);
     },
     "staging returns a valid secret": function(err, r) {
       assert.isNull(err);
@@ -85,8 +85,8 @@ suite.addBatch({
       topic: function(err, secret) {
         db.emailForVerificationSecret(secret, this.callback);
       },
-      "matches expected email": function(err, r) {
-        assert.strictEqual(r.email, 'lloyd@nowhe.re');
+      "matches expected email": function(err, email, uid) {
+        assert.strictEqual(email, 'lloyd@nowhe.re');
       }
     },
     "fetch secret for email": {
@@ -125,7 +125,7 @@ suite.addBatch({
 suite.addBatch({
   "upon receipt of a secret": {
     topic: function() {
-      db.gotVerificationSecret(secret, 'fakepasswordhash', this.callback);
+      db.gotVerificationSecret(secret, this.callback);
     },
     "gotVerificationSecret completes without error": function (err, r) {
       assert.isNull(err);
@@ -164,7 +164,7 @@ suite.addBatch({
     },
     "the correct password": function(err, r) {
       assert.isNull(err);
-      assert.strictEqual(r, "fakepasswordhash");
+      assert.strictEqual(r, "biglonghashofapassword");
     }
   }
 });
@@ -200,7 +200,7 @@ suite.addBatch({
     },
     "then staging an email": {
       topic: function(err, uid) {
-        db.stageEmail(uid, 'lloyd@somewhe.re', this.callback);
+        db.stageEmail(uid, 'lloyd@somewhe.re', 'biglonghashofapassword', this.callback);
       },
       "yields a valid secret": function(err, secret) {
         assert.isNull(err);
@@ -215,7 +215,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, undefined, this.callback);
+            db.gotVerificationSecret(secret, this.callback);
           },
           "successfully": function(err, r) {
             assert.isNull(err);
diff --git a/tests/email-throttling-test.js b/tests/email-throttling-test.js
index 4cceed04d5c83ee9c07d7a4e9bd3d87586d5207a..9b7e4560cfe8149e06d57272d946c9e042d3ee5d 100755
--- a/tests/email-throttling-test.js
+++ b/tests/email-throttling-test.js
@@ -24,6 +24,7 @@ suite.addBatch({
   "staging a registration": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'first@fakeemail.com',
+      pass: 'firstfakepass',
       site:'https://fakesite.com:443'
     }),
     "returns 200": function(err, r) {
@@ -49,6 +50,7 @@ suite.addBatch({
   "immediately staging another": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'first@fakeemail.com',
+      pass: 'firstfakepass',
       site:'http://fakesite.com:80'
     }),
     "is throttled": function(err, r) {
@@ -60,7 +62,7 @@ suite.addBatch({
 suite.addBatch({
   "finishing creating the first account": {
     topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'firstfakepass' }).call(this);
+      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
     },
     "works": function(err, r) {
       assert.equal(r.code, 200);
diff --git a/tests/forgotten-email-test.js b/tests/forgotten-email-test.js
index d08c98dad2cd25a056a07e14afa70443cdbbb6ef..c2e6aa3823be90f8c8093e013cc5697f09d6e67e 100755
--- a/tests/forgotten-email-test.js
+++ b/tests/forgotten-email-test.js
@@ -25,6 +25,7 @@ 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) {
@@ -49,7 +50,7 @@ suite.addBatch({
 suite.addBatch({
   "create first account": {
     topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'firstfakepass' }).call(this);
+      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
     },
     "account created": function(err, r) {
       assert.equal(r.code, 200);
@@ -137,6 +138,7 @@ 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) {
@@ -187,7 +189,7 @@ suite.addBatch({
 suite.addBatch({
   "re-create first email address": {
     topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'secondfakepass' }).call(this);
+      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
     },
     "account created": function(err, r) {
       assert.equal(r.code, 200);
@@ -196,7 +198,7 @@ suite.addBatch({
   }
 });
 
-// now we should be able to sign into the first email address with the second
+// 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": {
diff --git a/tests/list-emails-wsapi-test.js b/tests/list-emails-wsapi-test.js
index 09fa35775cd57cbfc53ae6fa6c475dc0839e7bce..037bed0b812225b6618a231c75f94ec25e12bf33 100755
--- a/tests/list-emails-wsapi-test.js
+++ b/tests/list-emails-wsapi-test.js
@@ -27,6 +27,7 @@ suite.addBatch({
   "stage an account": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'syncer@somehost.com',
+      pass: 'fakepass',
       site:'https://foobar.fakesite.com'
     }),
     "works": function(err, r) {
@@ -51,7 +52,7 @@ suite.addBatch({
 suite.addBatch({
   "verifying account ownership": {
     topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'fakepass' }).call(this);
+      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
     },
     "works": function(err, r) {
       assert.equal(r.code, 200);
diff --git a/tests/no-cookie-test.js b/tests/no-cookie-test.js
index 33a2db02f237d4a816503b19be3cdfbf01e611f9..1689b22946e2faeca777c710a3a7570b6f908283 100755
--- a/tests/no-cookie-test.js
+++ b/tests/no-cookie-test.js
@@ -27,6 +27,7 @@ suite.addBatch({
   "start registration": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'first@fakeemail.com',
+      pass: 'firstfakepass',
       site:'http://fakesite.com:123'
     }),
     "returns 200": function(err, r) {
@@ -51,7 +52,7 @@ suite.addBatch({
 suite.addBatch({
   "completing user creation": {
     topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'firstfakepass' }).call(this);
+      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
     },
     "works": function(err, r) {
       assert.equal(r.code, 200);
diff --git a/tests/password-bcrypt-update-test.js b/tests/password-bcrypt-update-test.js
index df5eae39034b6ef26c86fe86f4c3812e491f23ab..033e9109f29269645e38ae45e1c3476881c14616 100755
--- a/tests/password-bcrypt-update-test.js
+++ b/tests/password-bcrypt-update-test.js
@@ -46,6 +46,7 @@ suite.addBatch({
   "account staging": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: TEST_EMAIL,
+      pass: TEST_PASSWORD,
       site:'https://fakesite.com'
     }),
     "works":     function(err, r) {
@@ -72,8 +73,7 @@ suite.addBatch({
   "setting password": {
     topic: function() {
       wsapi.post('/wsapi/complete_user_creation', {
-        token: token,
-        pass: TEST_PASSWORD
+        token: token
       }).call(this);
     },
     "works just fine": function(err, r) {
diff --git a/tests/password-length-test.js b/tests/password-length-test.js
index 2294e3b306819392fdd67a96c18847ea8a5bfd81..dc7a2b02230b9dfac5438c093f1ab6a4369537a1 100755
--- a/tests/password-length-test.js
+++ b/tests/password-length-test.js
@@ -37,69 +37,50 @@ suite.addBatch({
 
 // first stage the account
 suite.addBatch({
-  "account staging": {
+  "a password that is non-existent": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'first@fakeemail.com',
       site:'https://fakesite.com:123'
     }),
-    "works":     function(err, r) {
-      assert.equal(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;
+    "causes a HTTP error response": function(err, r) {
+      assert.equal(r.code, 400);
+      assert.equal(r.body, "Bad Request: missing 'pass' argument");
     }
-  }
-});
-
-
-// create a new account via the api with (first address)
-suite.addBatch({
+  },
   "a password that is too short": {
-    topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', {
-        token: token,
-        pass: '0123456' // less than 8 chars, invalid
-      }).call(this)
-    },
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: 'first@fakeemail.com',
+      pass: '0123456', // less than 8 chars, invalid
+      site:'https://fakesite.com:123'
+    }),
     "causes a HTTP error response": function(err, r) {
       assert.equal(r.code, 400);
       assert.equal(r.body, "Bad Request: valid passwords are between 8 and 80 chars");
     }
   },
   "a password that is too long": {
-    topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', {
-        token: token,
-        pass: '012345678901234567890123456789012345678901234567890123456789012345678901234567891', // more than 81 chars, invalid.
-      }).call(this);
-    },
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: 'first@fakeemail.com',
+      pass: '012345678901234567890123456789012345678901234567890123456789012345678901234567891', // more than 81 chars, invalid.
+      site:'https://fakesite.com:123'
+    }),
     "causes a HTTP error response": function(err, r) {
       assert.equal(r.code, 400);
       assert.equal(r.body, "Bad Request: valid passwords are between 8 and 80 chars");
     }
   },
   "but a password that is just right": {
-    topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', {
-        token: token,
-        pass: 'ahhh.  this is just right.'
-      }).call(this);
-    },
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: 'first@fakeemail.com',
+      pass: 'ahhh.  this is just right.',
+      site:'https://fakesite.com:123'
+    }),
     "works just fine": function(err, r) {
       assert.equal(r.code, 200);
     }
   }
 });
+
 start_stop.addShutdownBatches(suite);
 
 // run or export the suite.
diff --git a/tests/password-update-test.js b/tests/password-update-test.js
index b04a4951fd7bf0f2a1161a56fc7dc02a92b41c47..98ca7384226b05d41c59f7f245209544bbce2eee 100755
--- a/tests/password-update-test.js
+++ b/tests/password-update-test.js
@@ -34,6 +34,7 @@ suite.addBatch({
   "account staging": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: TEST_EMAIL,
+      pass: OLD_PASSWORD,
       site: 'https://fakesite.com:123'
     }),
     "works":     function(err, r) {
@@ -60,8 +61,7 @@ suite.addBatch({
   "setting password": {
     topic: function() {
       wsapi.post('/wsapi/complete_user_creation', {
-        token: token,
-        pass: OLD_PASSWORD
+        token: token
       }).call(this);
     },
     "works just fine": function(err, r) {
diff --git a/tests/primary-then-secondary-test.js b/tests/primary-then-secondary-test.js
index 11104396191720812da44e195cb6b2020aea0751..c09e023b04c68520d03c5229878611cbed93d32c 100755
--- a/tests/primary-then-secondary-test.js
+++ b/tests/primary-then-secondary-test.js
@@ -75,7 +75,6 @@ suite.addBatch({
 });
 
 var token;
-
 // now we have a new account.  let's add a secondary to it
 suite.addBatch({
   "add a new email address to our account": {
@@ -83,40 +82,37 @@ suite.addBatch({
       email: SECONDARY_EMAIL,
       site:'https://fakesite.com'
     }),
-    "works": function(err, r) {
+    "fails without a password": function(err, r) {
       assert.strictEqual(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).success, false);
     },
-    "and get a token": {
-      topic: function() {
-        start_stop.waitForToken(this.callback);
-      },
-      "successfully": function (t) {
-        this._token = t;
-        assert.strictEqual(typeof t, 'string');
+    "with a password": {
+      topic: wsapi.post('/wsapi/stage_email', {
+        email: SECONDARY_EMAIL,
+        pass: TEST_PASS,
+        site:'https://fakesite.com'
+      }),
+      "succeeds": function(err, r) {
+        assert.strictEqual(r.code, 200);
       },
-      "and complete":  {
-        topic: function(t) {
-          wsapi.get('/wsapi/email_for_token', {
-            token: t
-          }).call(this);
+      "and get a token": {
+        topic: function() {
+          start_stop.waitForToken(this.callback);
         },
-        "we need to set our password": function (err, r) {
-          r = JSON.parse(r.body);
-          assert.ok(r.needs_password);
+        "successfully": function (t) {
+          this._token = t;
+          assert.strictEqual(typeof t, 'string');
         },
-        "with": {
-          topic: function() {
-            wsapi.post('/wsapi/complete_email_addition', { token: this._token }).call(this);
+        "and complete":  {
+          topic: function(t) {
+            wsapi.get('/wsapi/email_for_token', {
+              token: t
+            }).call(this);
           },
-          "no password fails": function(err, r) {
-            assert.equal(r.code, 200);
-            assert.strictEqual(JSON.parse(r.body).success, false);
-          },
-          "a password": {
+          "which then": {
             topic: function() {
               wsapi.post('/wsapi/complete_email_addition', {
-                token: this._token,
-                pass: TEST_PASS
+                token: this._token
               }).call(this);
             },
             "succeeds": function(err, r) {
@@ -140,62 +136,58 @@ suite.addBatch({
   }
 });
 
-
-// after a small delay, we can authenticate with our password
+// now we can authenticate with our password
 suite.addBatch({
-  "after a small delay": {
-    topic: function() { setTimeout(this.callback, 1500); },
-    "authenticating with our newly set password" : {
-      topic: wsapi.post('/wsapi/authenticate_user', {
-        email: TEST_EMAIL,
-        pass: TEST_PASS,
-        ephemeral: false
-      }),
-      "works": function(err, r) {
-        assert.strictEqual(r.code, 200);
-      }
+  "authenticating with our newly set password" : {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: TEST_EMAIL,
+      pass: TEST_PASS,
+      ephemeral: false
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
     }
   }
 });
 
-
 // adding a second secondary will not let us set the password
 suite.addBatch({
-  "add a new email address to our account": {
+  "add a second secondary to account with": {
     topic: wsapi.post('/wsapi/stage_email', {
       email: SECOND_SECONDARY_EMAIL,
+      pass: TEST_PASS,
       site:'http://fakesite.com:123'
     }),
-    "works": function(err, r) {
+    "a password fails": function(err, r) {
       assert.strictEqual(r.code, 200);
+      var body = JSON.parse(r.body);
+      assert.strictEqual(body.success, false);
+      assert.strictEqual(body.reason, 'a password may not be set at this time');
     },
-    "and get a token": {
-      topic: function() {
-        start_stop.waitForToken(this.callback);
-      },
-      "successfully": function (t) {
-        this._token = t;
-        assert.strictEqual(typeof t, 'string');
+    "but with no password specified": {
+      topic: wsapi.post('/wsapi/stage_email', {
+        email: SECOND_SECONDARY_EMAIL,
+        site:'http://fakesite.com:123'
+      }),
+      "succeeds": function(err, r) {
+        assert.strictEqual(r.code, 200);
+        assert.strictEqual(JSON.parse(r.body).success, true);
       },
-      "and to complete": {
-        topic: function(t) {
-          wsapi.get('/wsapi/email_for_token', {
-            token: t
-          }).call(this);
+      "and get a token": {
+        topic: function() {
+          start_stop.waitForToken(this.callback);
         },
-        "we do not need to set our password": function (err, r) {
-          r = JSON.parse(r.body);
-          assert.isFalse(r.needs_password);
+        "successfully": function (t) {
+          this._token = t;
+          assert.strictEqual(typeof t, 'string');
         },
-        "with": {
-          topic: function() {
-            wsapi.post('/wsapi/complete_email_addition', { token: this._token, pass: TEST_PASS }).call(this);
+        "and to complete":  {
+          topic: function(t) {
+            wsapi.get('/wsapi/email_for_token', {
+              token: t
+            }).call(this);
           },
-          "a password fails": function(err, r) {
-            assert.equal(r.code, 200);
-            assert.strictEqual(JSON.parse(r.body).success, false);
-          },
-          "no password succeeds": {
+          "with a token": {
             topic: function() {
               wsapi.post('/wsapi/complete_email_addition', {
                 token: this._token
@@ -231,11 +223,10 @@ suite.addBatch({
     }),
     "works": function(err, r) {
       assert.strictEqual(r.code, 200);
-    },
+    }
   }
 });
 
-
 // shut the server down and cleanup
 start_stop.addShutdownBatches(suite);
 
diff --git a/tests/registration-status-wsapi-test.js b/tests/registration-status-wsapi-test.js
index 9857e9e39a80172b4a8852e79c15ccb3280a7e2d..10944b1ae2388549ddf83fad688ca8fff0996228 100755
--- a/tests/registration-status-wsapi-test.js
+++ b/tests/registration-status-wsapi-test.js
@@ -51,6 +51,7 @@ suite.addBatch({
   "start registration": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'first@fakeemail.com',
+      pass: 'firstfakepass',
       site:'https://fakesite.com'
     }),
     "returns 200": function(err, r) {
@@ -110,7 +111,7 @@ suite.addBatch({
 suite.addBatch({
   "completing user creation": {
     topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'firstfakepass' }).call(this);
+      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
     },
     "works": function(err, r) {
       assert.equal(r.code, 200);
@@ -170,6 +171,7 @@ suite.addBatch({
   "re-registering an existing email": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'first@fakeemail.com',
+      pass: 'secondfakepass',
       site:'http://secondfakesite.com'
     }),
     "yields a HTTP 200": function (err, r) {
@@ -206,7 +208,7 @@ suite.addBatch({
 suite.addBatch({
   "proving email ownership causes account re-creation": {
     topic: function() {
-      wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'secondfakepass' }).call(this);
+      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
     },
     "and returns a 200 code": function(err, r) {
       assert.equal(r.code, 200);
diff --git a/tests/session-context-test.js b/tests/session-context-test.js
index 87a42ac40ce34faf8658bcf6a5b20b68b232eb61..6c2fe805a1c2a519ff1a7a9bccb434f018c06e5a 100755
--- a/tests/session-context-test.js
+++ b/tests/session-context-test.js
@@ -32,6 +32,7 @@ suite.addBatch({
   "account staging": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: TEST_EMAIL,
+      pass: PASSWORD,
       site: 'https://fakesite.com'
     }),
     "works":     function(err, r) {
@@ -58,8 +59,7 @@ suite.addBatch({
   "setting password": {
     topic: function() {
       wsapi.post('/wsapi/complete_user_creation', {
-        token: token,
-        pass: PASSWORD
+        token: token
       }).call(this);
     },
     "works just fine": function(err, r) {
@@ -89,7 +89,7 @@ suite.addBatch({
       var resp = JSON.parse(r.body);
       assert.strictEqual(typeof resp.csrf_token, 'string');
       var serverTime = new Date(resp.server_time);
-      assert.ok(new Date() - serverTime < 5000);      
+      assert.ok(new Date() - serverTime < 5000);
       assert.strictEqual(resp.authenticated, true);
       assert.strictEqual(resp.auth_level, 'password');
       var domainKeyCreation = new Date(resp.domain_key_creation_time);
diff --git a/tests/session-duration-test.js b/tests/session-duration-test.js
index cedec0f0857870940ede1492dfdbad94beae19bc..232ad6320a6f7979a6f5f5d39f800eb377229a9a 100755
--- a/tests/session-duration-test.js
+++ b/tests/session-duration-test.js
@@ -106,6 +106,7 @@ suite.addBatch({
   "account staging": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: TEST_EMAIL,
+      pass: PASSWORD,
       site: 'http://a.really.fakesite123.com:999'
     }),
     "works":     function(err, r) {
@@ -132,8 +133,7 @@ suite.addBatch({
   "setting password": {
     topic: function() {
       wsapi.post('/wsapi/complete_user_creation', {
-        token: token,
-        pass: PASSWORD
+        token: token
       }).call(this);
     },
     "works just fine": function(err, r) {
diff --git a/tests/session-prolong-test.js b/tests/session-prolong-test.js
index adf193e2764b7d6adb53aa9d7603a585e1ef0dce..2df5cc4d2a2a7e1ad80cb847f233479389f5e9b5 100755
--- a/tests/session-prolong-test.js
+++ b/tests/session-prolong-test.js
@@ -32,6 +32,7 @@ suite.addBatch({
   "account staging": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: TEST_EMAIL,
+      pass: PASSWORD,
       site: 'http://fakesite.com'
     }),
     "works":     function(err, r) {
@@ -58,8 +59,7 @@ suite.addBatch({
   "setting password": {
     topic: function() {
       wsapi.post('/wsapi/complete_user_creation', {
-        token: token,
-        pass: PASSWORD
+        token: token
       }).call(this);
     },
     "works just fine": function(err, r) {
diff --git a/tests/stalled-mysql-test.js b/tests/stalled-mysql-test.js
index 0bdd2b4b4b670b034120f3f47c886d5ced31bac8..e75fbad678cf03961d3a2a32bafe626768e4b1df 100755
--- a/tests/stalled-mysql-test.js
+++ b/tests/stalled-mysql-test.js
@@ -130,7 +130,7 @@ suite.addBatch({
   "complete_user_creation": {
     topic: wsapi.post('/wsapi/complete_user_creation', {
       token: 'bogus',
-      pass: 'fakefake'
+      pass: 'alsobogus'
     }),
     "fails with 503": function(err, r) {
       assert.strictEqual(r.code, 503);
@@ -147,6 +147,7 @@ suite.addBatch({
   "stage_user": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'bogus@bogus.edu',
+      pass: 'a_password',
       site: 'https://whatev.er'
     }),
     "fails with 503": function(err, r) {
@@ -176,6 +177,7 @@ suite.addBatch({
   "account staging": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: "stalltest@whatev.er",
+      pass: 'a_password',
       site: 'http://fakesite.com'
     }),
     "works":     function(err, r) {
@@ -195,8 +197,7 @@ suite.addBatch({
     "setting password": {
       topic: function(token) {
         wsapi.post('/wsapi/complete_user_creation', {
-          token: token,
-          pass: "somepass"
+          token: token
         }).call(this);
       },
       "works just fine": function(err, r) {
@@ -266,6 +267,7 @@ suite.addBatch({
   "stage_email": {
     topic: wsapi.post('/wsapi/stage_email', {
       email: "test2@whatev.er",
+      pass: 'a_password',
       site: "https://foo.com"
     }),
     "fails with 503": function(err, r) {
diff --git a/tests/verifier-test.js b/tests/verifier-test.js
index 09ea032801dc5fdff489040a1b281666efdb0a47..efa098a102eec4110daff9b8dc582218e1e2eb6d 100755
--- a/tests/verifier-test.js
+++ b/tests/verifier-test.js
@@ -41,6 +41,7 @@ suite.addBatch({
   "account staging": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: TEST_EMAIL,
+      pass: TEST_PASSWORD,
       site: TEST_ORIGIN
     }),
     "works":     function(err, r) {
@@ -65,8 +66,7 @@ suite.addBatch({
   "setting password and creating the account": {
     topic: function() {
       wsapi.post('/wsapi/complete_user_creation', {
-        token: token,
-        pass: TEST_PASSWORD
+        token: token
       }).call(this);
     },
     "works just fine": function(err, r) {
@@ -714,7 +714,7 @@ suite.addBatch({
 });
 
 // now verify that assertions from a primary who does not have browserid support
-// will fail to verify 
+// will fail to verify
 function make_other_issuer_tests(new_style) {
   var title = "generating an assertion from a cert signed by some other domain with " + (new_style ? "new style" : "old style");
   var tests = {
diff --git a/tests/verify-in-different-browser-test.js b/tests/verify-in-different-browser-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..6ebd2fe15cbf02556009865a404aa998d831ea7b
--- /dev/null
+++ b/tests/verify-in-different-browser-test.js
@@ -0,0 +1,353 @@
+#!/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'),
+primary = require('./lib/primary.js');
+
+var suite = vows.describe('verify-in-different-browser');
+
+// start up a pristine server
+start_stop.addStartupBatches(suite);
+
+// This test ensures that when email verification of a secondary address
+// occurs in a browsing context other than the one that initiated it,
+// the user must re-provide their password.
+
+// first we'll need to authenticate a user with an assertion from a
+// primary IdP
+
+const TEST_DOMAIN = 'example.domain',
+      TEST_EMAIL = 'testuser@' + TEST_DOMAIN,
+      TEST_ORIGIN = 'http://127.0.0.1:10002',
+      TEST_PASS = 'fakepass',
+      SECONDARY_EMAIL = 'secondary@notexample.domain',
+      SECOND_SECONDARY_EMAIL = 'secondsecondary@notexample.domain',
+      THIRD_SECONDARY_EMAIL = 'thirdsecondary@notexample.domain',
+      FOURTH_SECONDARY_EMAIL = 'fourthsecondary@notexample.domain';
+
+var primaryUser = new primary({
+  email: TEST_EMAIL,
+  domain: TEST_DOMAIN
+});
+
+// first we'll create an account without a password by using
+// a primary address.
+suite.addBatch({
+  "generating an assertion": {
+    topic: function() {
+      return primaryUser.getAssertion(TEST_ORIGIN);
+    },
+    "succeeds": function(r) {
+      assert.isString(r);
+    },
+    "and logging in with the assertion": {
+      topic: function(assertion)  {
+        wsapi.post('/wsapi/auth_with_assertion', {
+          assertion: assertion,
+          ephemeral: true
+        }).call(this);
+      },
+      "succeeds": function(err, r) {
+        var resp = JSON.parse(r.body);
+        assert.isObject(resp);
+        assert.isTrue(resp.success);
+      }
+    }
+  }
+});
+
+var token;
+
+// let's add a secondary email to this account
+suite.addBatch({
+  "add a new email address to our account": {
+    topic: wsapi.post('/wsapi/stage_email', {
+      email: SECONDARY_EMAIL,
+      pass: TEST_PASS,
+      site:'https://fakesite.com'
+    }),
+    "succeeds": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    },
+    "and get a token": {
+      topic: function() {
+        start_stop.waitForToken(this.callback);
+      },
+      "successfully": function (t) {
+        this._token = t;
+        assert.strictEqual(typeof t, 'string');
+      },
+      "then clearing cookies and completing": {
+        topic: function() {
+          wsapi.clearCookies();
+          wsapi.post('/wsapi/complete_email_addition', {
+            token: this._token
+          }).call(this);
+        },
+        "fails without a password": function(err, r) {
+          assert.strictEqual(r.code, 401);
+        },
+        "but succeeds": {
+          topic: function() {
+            wsapi.post('/wsapi/complete_email_addition', {
+              token: this._token,
+              pass: TEST_PASS
+            }).call(this);
+          },
+          "with one": function(err, r) {
+            assert.strictEqual(r.code, 200);
+          }
+        }
+      }
+    }
+  }
+});
+
+// after adding a secondary and setting password, we're password auth'd
+suite.addBatch({
+  "auth_level": {
+    topic: wsapi.get('/wsapi/session_context'),
+    "is 'password' after authenticating with password" : function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).auth_level, 'password');
+    }
+  }
+});
+
+// we can authenticate with our password
+suite.addBatch({
+  "authenticating with our newly set password" : {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: TEST_EMAIL,
+      pass: TEST_PASS,
+      ephemeral: false
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// let's add another secondary email, again by confirming the address on
+// "a different browser".  This time, the server will have to authenticate
+// us by pulling our password out of our user record rather than out of
+// the stage table.
+suite.addBatch({
+  "add a new email address to our account": {
+    topic: wsapi.post('/wsapi/stage_email', {
+      email: SECOND_SECONDARY_EMAIL,
+      site:'https://fakesite.com'
+    }),
+    "succeeds": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    },
+    "and get a token": {
+      topic: function() {
+        start_stop.waitForToken(this.callback);
+      },
+      "successfully": function (t) {
+        this._token = t;
+        assert.strictEqual(typeof t, 'string');
+      },
+      "then clearing cookies and completing": {
+        topic: function() {
+          wsapi.clearCookies();
+          wsapi.post('/wsapi/complete_email_addition', {
+            token: this._token
+          }).call(this);
+        },
+        "fails without a password": function(err, r) {
+          assert.strictEqual(r.code, 401);
+        },
+        "but succeeds": {
+          topic: function() {
+            wsapi.post('/wsapi/complete_email_addition', {
+              token: this._token,
+              pass: TEST_PASS
+            }).call(this);
+          },
+          "with one": function(err, r) {
+            assert.strictEqual(r.code, 200);
+          }
+        }
+      }
+    }
+  }
+});
+
+// we're password auth'd
+suite.addBatch({
+  "auth_level": {
+    topic: wsapi.get('/wsapi/session_context'),
+    "is 'password' after authenticating with password" : function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).auth_level, 'password');
+    }
+  }
+});
+
+
+// we can still authenticate with our password
+suite.addBatch({
+  "authenticating with our newly set password" : {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: TEST_EMAIL,
+      pass: TEST_PASS,
+      ephemeral: false
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// now we've tested proper restrictions on the add email, flow, how about
+// new account creation?
+
+// creating a new account and verifying in "a different browser" requires password
+suite.addBatch({
+  "staging a new account": {
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: THIRD_SECONDARY_EMAIL,
+      pass:  TEST_PASS,
+      site:  'http://fakesite.com:1235'
+    }),
+    "succeeds": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).success, true);
+    },
+    "yields a token": {
+      topic: function() {
+        start_stop.waitForToken(this.callback);
+      },
+      "successfully": function (t) {
+        this._token = t;
+        assert.strictEqual(typeof t, 'string');
+      },
+      "then clearing cookies and completing": {
+        topic: function() {
+          wsapi.clearCookies();
+          wsapi.post('/wsapi/complete_user_creation', {
+            token: this._token
+          }).call(this);
+        },
+        "fails without a password": function(err, r) {
+          assert.strictEqual(r.code, 401);
+        },
+        "but succeeds": {
+          topic: function() {
+            wsapi.post('/wsapi/complete_email_addition', {
+              token: this._token,
+              pass: TEST_PASS
+            }).call(this);
+          },
+          "with one": function(err, r) {
+            assert.strictEqual(r.code, 200);
+          }
+        }
+      }
+    }
+  }
+});
+
+// creating a new account and verifying in "the same browser" requires no password
+suite.addBatch({
+  "staging a new account": {
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: FOURTH_SECONDARY_EMAIL,
+      pass:  TEST_PASS,
+      site:  'http://fakesite.com:1235'
+    }),
+    "succeeds": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).success, true);
+    },
+    "yields a token": {
+      topic: function() {
+        start_stop.waitForToken(this.callback);
+      },
+      "successfully": function (t) {
+        this._token = t;
+        assert.strictEqual(typeof t, 'string');
+      },
+      "and completion with only a token": {
+        topic: function() {
+          wsapi.post('/wsapi/complete_user_creation', {
+            token: this._token
+          }).call(this);
+        },
+        "succeeds": function(err, r) {
+          assert.strictEqual(r.code, 200);
+        }
+      }
+    }
+  }
+});
+
+suite.addBatch({
+  "authentication with first email": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: TEST_EMAIL,
+      pass: TEST_PASS,
+      ephemeral: false
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    },
+  },
+  "authentication with second email": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: SECONDARY_EMAIL,
+      pass: TEST_PASS,
+      ephemeral: false
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  },
+  "authentication with third email": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: SECOND_SECONDARY_EMAIL,
+      pass: TEST_PASS,
+      ephemeral: false
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  },
+  "authentication with fourth email": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: THIRD_SECONDARY_EMAIL,
+      pass: TEST_PASS,
+      ephemeral: false
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  },
+  "authentication with fifth email": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: FOURTH_SECONDARY_EMAIL,
+      pass: TEST_PASS,
+      ephemeral: false
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+
+// shut the server down and cleanup
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);