diff --git a/ChangeLog b/ChangeLog
index a36cfb8104fb573403df2b92b89a3ba4610fff1c..34982fe02366f3ad0301e6e79cca11328c7b7fee 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,35 @@
-train-2012.07.20: (in progress)
-  * 
+train-2012.08.03 (in progress):
+
+
+train-2012.07.20:
+  * Introduction of "static" process which serves views and static resources: #1757
+  * Update account password recovery flow, no longer do we remove all emails upon password recovery: #1913
+  * API parameter validation on all API calls: #1526, #2001, #1981, #2042, #2032, #2057, #2121
+  * 'locale_directory' no longer a neccesary nor allowed configuration parameter
+  * Implement support for proxy IdP (a.k.a. BigTent): #2019, #2060
+  * Main site i18n - now persona is completely translated: #1862, #2075, #2093
+  * UI improvements: #1898, #1786, #1920, #1932, #1901, #1885, #1951, #1964, #1967, #1916, #1967, #2007
+  * KPI improvements: #1827, #1825
+  * Localization improvments, reduced dependencies and debugging locale works all the time: #1917, #1905, #1970
+  * Fix regression of fonts on windows: #1856, #1973
+  * Resource optimization: #1941, #1999
+  * Links to external sumo pages are language neutral: #1938, #2055
+  * Unit test fixes and improvements: #1958, #1948, #1783, #1916, #2011, #1986
+  * Fixes for node 0.8.x (production still on node 0.6.x): #1914
+  * Code cleanup: #1902, #1989
+  * Language improvements: #1960, #1167
+  * Opera 12 fixes: #1844
+  * Persona logos added to repo: #1974
+  * Fix error when KPIs are disabled: #1978
+  * For primary certificate provisioning, fail if the process takes longer than 20s: #1570
+  * Fix IE8 cookie check: #1982
+  * Log assertion verification failures: #2016
+  * Fix slow keyboard key press response on fennec: #2029
+  * Documentation fixes: #2064
+  * All resources should include license and links: #1655?
+  * Repair metrics, specifically counting of distinct sign_ins: #2040
+  * returnTo, siteName, and siteLogo only work with the observer API: #2086
+  * Fix regressions introduced during development: #2118, #2104, #2088
 
 train-2012.07.06:
   * refinement of all user facing language: #1889, #1905, #1675, #1923, #1925
diff --git a/bin/router b/bin/router
index 1d333c310362f4d09ab8dd9b2873711b02a85f0d..6b27d7ead72cd0c0901acfb6d693962cbd7df46a 100755
--- a/bin/router
+++ b/bin/router
@@ -13,6 +13,7 @@ urlparse = require('urlparse'),
 express = require('express');
 
 const
+metrics = require('../lib/metrics.js'),
 wsapi = require('../lib/wsapi.js'),
 config = require('../lib/configuration.js'),
 heartbeat = require('../lib/heartbeat.js'),
@@ -146,6 +147,10 @@ wsapi.routeSetup(app, {
 
 //catch-all
 app.use(function(req, res, next) {
+
+  // log metrics
+  if (req.url === '/sign_in') metrics.userEntry(req);
+
   forward(
     static_url+req.url, req, res,
     function(err) {
diff --git a/bin/static b/bin/static
index d9f3894084f03958960848d9ffcc60ec085f09ef..adbcdedd6d9950470f62ac332f0ba283a5d85b0e 100755
--- a/bin/static
+++ b/bin/static
@@ -90,11 +90,15 @@ app.use(cachify.setup(assets(config.get('supported_languages')),
           root: static_root,
         }));
 
-
-// if nothing else has caught this request, serve static files, but ensure
-// that proper vary headers are installed to prevent unwanted caching
+// add 'Access-Control-Allow-Origin' headers to static resources that will be served
+// from the CDN.  We explicitly allow resources served from public_url to access these.
 app.use(function(req, res, next) {
-  res.setHeader('Vary', 'Accept-Encoding,Accept-Language');
+  res.on('header', function() {
+    // this allows fonts to be requested cross domain
+    res.setHeader("Access-Control-Allow-Origin", config.get('public_url'));
+    // this makes sure caches properly consider language headers
+    res.setHeader('Vary', 'Accept-Encoding,Accept-Language');
+  });
   next();
 });
 
diff --git a/docs/AWS_DEPLOYMENT.md b/docs/AWS_DEPLOYMENT.md
index 5d31edb280e8b3562dd4626670a970b7846be81e..65013b4760d13db067a3c1341bbfb9784ca32cf4 100644
--- a/docs/AWS_DEPLOYMENT.md
+++ b/docs/AWS_DEPLOYMENT.md
@@ -173,3 +173,16 @@ access to:
 Feel free to start a new server, and ssh in as `app` to explore all of the
 configuration.  An attempt has been made to isolate as much configuration 
 under this user's account as possible.
+
+### Hacking the deployed code
+
+If you want to change anything on your VM, you should really just commit to
+your local git repo and then push the changes over to the EC2 instance.
+
+However, sometimes that doesn't work for some reason and you need to hack
+the code directly and restart the services:
+
+  1. ssh into the VM as the `app` user
+  2. hack the currently running code in `/home/app/code/`
+  3. run the js combiner/minifier: `/home/app/code/scripts/compress`
+  4. restart all of the services: `forever restartall`
diff --git a/example/rp/index.html b/example/rp/index.html
index 50f5808a8da6400b14728ef5d541909b156e29a3..bef670ebd017a23aec6d333a0f25362f3a7f8b33 100644
--- a/example/rp/index.html
+++ b/example/rp/index.html
@@ -193,7 +193,7 @@ $(document).ready(function() {
     });
   });
 
-  $(".specify button.logout").click(navigator.id.logout);
+  $(".specify button.logout").click(function() { navigator.id.logout() });
 
   $(".session button.update_session").click(function() {
     storage.loggedInUser = $.trim($('#loggedInUser').val());
diff --git a/lib/load_gen/activities/reset_pass.js b/lib/load_gen/activities/reset_pass.js
index d6ccbaf46c5872c9cf16e6b823658aafc742cc07..100f134865e477ad7bd063e819af62552b043b2e 100644
--- a/lib/load_gen/activities/reset_pass.js
+++ b/lib/load_gen/activities/reset_pass.js
@@ -77,8 +77,7 @@ exports.startFunc = function(cfg, cb) {
 
       // and simulate clickthrough
       wcli.post(cfg, '/wsapi/complete_user_creation', context, {
-        token: r.body,
-        ephemeral: false
+        token: r.body
       }, function (err, r) {
         if (err) {
           return cb(err);
diff --git a/lib/load_gen/activities/signup.js b/lib/load_gen/activities/signup.js
index d628756660aef5bcddd024b478e40844defe0e8a..51b0678b28bf5f21033699a6cbe1eb03be279a71 100644
--- a/lib/load_gen/activities/signup.js
+++ b/lib/load_gen/activities/signup.js
@@ -74,8 +74,7 @@ exports.startFunc = function(cfg, cb) {
       if (r.code !== 200) return cb("can't get verification secret: " + r.code);
       // and simulate clickthrough
       wcli.post(cfg, '/wsapi/complete_user_creation', context, {
-        token: r.body,
-        ephemeral: false
+        token: r.body
       }, function (err, r) {
         try {
           if (err) throw err;
diff --git a/lib/metrics.js b/lib/metrics.js
index c5e2cd2b5105b8b9fa2700f7e1c781b4cb2e8434..eeabd14d8b73d72e75d1134f94926cd24433b646 100644
--- a/lib/metrics.js
+++ b/lib/metrics.js
@@ -56,6 +56,9 @@ function setupLogger() {
     mkdir_p(log_path);
 
   var filename = path.join(log_path, configuration.get('process_type') + "-metrics.json");
+  if (process.env.METRICS_LOG_FILE) {
+    filename = process.env.METRICS_LOG_FILE;
+  }
 
   LOGGER = new (winston.Logger)({
       transports: [new (winston.transports.File)({filename: filename})],
diff --git a/lib/static/views.js b/lib/static/views.js
index e3779900b728bb6c04f5f1d66f3e0e4851fa9855..af0f9f7763b9bafa24a56cc56e53365995120981 100644
--- a/lib/static/views.js
+++ b/lib/static/views.js
@@ -98,7 +98,6 @@ exports.setup = function(app) {
   // this should probably be an internal redirect
   // as soon as relative paths are figured out.
   app.get('/sign_in', function(req, res, next ) {
-    metrics.userEntry(req);
     renderCachableView(req, res, 'dialog.ejs', {
       title: _('A Better Way to Sign In'),
       layout: 'dialog_layout.ejs',
@@ -108,7 +107,6 @@ exports.setup = function(app) {
   });
 
   app.get('/communication_iframe', function(req, res, next ) {
-
     renderCachableView(req, res, 'communication_iframe.ejs', {
       layout: false,
       production: config.get('use_minified_resources')
@@ -233,7 +231,7 @@ exports.setup = function(app) {
 
   // REDIRECTS
   const REDIRECTS = {
-    "/developers" : "https://developer.mozilla.org/en/BrowserID/Quick_Setup"
+    "/developers" : "https://developer.mozilla.org/en/BrowserID"
   };
 
   // set up all the redirects
diff --git a/lib/wsapi/complete_reset.js b/lib/wsapi/complete_reset.js
index 4d3bcfec2b762c39d535d8938f9b36e37e2fecba..49d8b2c58a342ffeb1339aeb3b51738f2489d1c1 100644
--- a/lib/wsapi/complete_reset.js
+++ b/lib/wsapi/complete_reset.js
@@ -33,8 +33,7 @@ exports.process = function(req, res) {
   //    request
 
   // is this the same browser?
-  if (typeof req.session.pendingReset === 'string' &&
-      req.params.token === req.session.pendingReset) {
+  if (req.params.token === req.session.pendingReset) {
     return postAuthentication();
   }
   // is a password provided?
diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js
index f737e3b8f297bc8873070dcc4b2a2d905b6e9fd4..66955d37849514d1ed8a83351507b8458bc5cc8a 100644
--- a/lib/wsapi/complete_user_creation.js
+++ b/lib/wsapi/complete_user_creation.js
@@ -38,8 +38,7 @@ exports.process = function(req, res) {
   // the email address of the attacked.
 
   // is this the same browser?
-  if (typeof req.session.pendingCreation === 'string' &&
-      req.params.token === req.session.pendingCreation) {
+  if (req.params.token === req.session.pendingCreation) {
     return postAuthentication();
   }
   // is a password provided?
diff --git a/lib/wsapi/email_for_token.js b/lib/wsapi/email_for_token.js
index 3aed92174cf1c5b138638959cffea8a223355803..5b28e07c1febb5460fce91db5a3a82e46c7fb90c 100644
--- a/lib/wsapi/email_for_token.js
+++ b/lib/wsapi/email_for_token.js
@@ -38,29 +38,28 @@ exports.process = function(req, res) {
           reason: err
         });
       }
-    } 
+    }
 
     function checkMustAuth() {
-      // must the user authenticate?  This is true if they are not authenticated
-      // as the uid who initiated the verification, and they are not on the same
-      // browser as the initiator
       var must_auth = true;
 
-      if (uid && req.session.userid === uid) {
-        must_auth = false;
-      }
-      else if (!uid && typeof req.session.pendingCreation === 'string' &&
-               req.params.token === req.session.pendingCreation) {
-        must_auth = false;
-      }
-      else if (typeof req.session.pendingReset === 'string' &&
-               req.params.token === req.session.pendingReset)
+      // For the following cases, the user must re-authenticate if they're not on the
+      // same browser.
+      // 1. they're resetting their password
+      // 2. they're creating their account
+      must_auth =
+        !((req.params.token === req.session.pendingCreation) ||
+          (req.params.token === req.session.pendingReset));
+
+      // For the following cases, unless the user is on the same browser AND authenticated,
+      // they must re-provide their password:
+      // 1. they're re-verifying an email after password reset
+      // 2. they're confirming a new email they want to add to their account
+      if (req.params.token === req.session.pendingReverification ||
+          req.params.token === req.session.pendingAddition)
       {
-        must_auth = false;
+        must_auth = !(req.session.userid && req.session.userid === uid);
       }
-      // NOTE: for reverification, we require you're authenticated.  it's not enough
-      // to be on the same browser - that path is nonsensical because you must be
-      // authenticated to initiate a re-verification.
 
       res.json({
         success: true,
diff --git a/lib/wsapi/password_reset_status.js b/lib/wsapi/password_reset_status.js
index e82b2f1dfb6d59dec8e88fe3b9b7412ab95ca6a3..67fdc1f9e92ee2d4042c1aaf754cf15f2d94fbe0 100644
--- a/lib/wsapi/password_reset_status.js
+++ b/lib/wsapi/password_reset_status.js
@@ -23,7 +23,7 @@ exports.process = function(req, res) {
   //   * if we are not authenticated as the owner of the email, we must auth
   db.isStaged(email, function(err, staged) {
     if (err) wsapi.databaseDown(res, err);
-    
+
     if (staged) {
       return res.json({ status: 'pending' });
     } else {
diff --git a/resources/static/common/js/network.js b/resources/static/common/js/network.js
index 190b3396df94d5d4be4e61abeff2789576075b88..ce04a07a463b422d96ca3d46051ab15307af24b7 100644
--- a/resources/static/common/js/network.js
+++ b/resources/static/common/js/network.js
@@ -101,15 +101,8 @@ BrowserID.Network = (function() {
   }
 
   function handleAddressVerifyCheckResponse(onComplete, status, textStatus, jqXHR) {
-    if (status.status === 'complete') {
-      // The user at this point can ONLY be logged in with password
-      // authentication. Once the registration is complete, that means
-      // the server has updated the user's cookies and the user is
-      // officially authenticated.
-      auth_status = 'password';
-
-      if (status.userid) setUserID(status.userid);
-    }
+    if (status.status === 'complete' && status.userid)
+      setUserID(status.userid);
     complete(onComplete, status.status);
   }
 
diff --git a/resources/static/common/js/user.js b/resources/static/common/js/user.js
index babc592290d3ce01c4e62e3c36aad20757d842eb..c5433499a1e4f4a4ae765dc7d0a8ddb6f7fbfaeb 100644
--- a/resources/static/common/js/user.js
+++ b/resources/static/common/js/user.js
@@ -17,7 +17,9 @@ BrowserID.User = (function() {
       addressCache = {},
       primaryAuthCache = {},
       complete = bid.Helpers.complete,
-      registrationComplete = false;
+      registrationComplete = false,
+      POLL_DURATION = 3000,
+      pollDuration = POLL_DURATION;
 
   function prepareDeps() {
     if (!jwcrypto) {
@@ -114,6 +116,12 @@ BrowserID.User = (function() {
     complete(onComplete, status);
   }
 
+  function markAddressVerified(email) {
+    var idInfo = storage.getEmail(email) || {};
+    idInfo.verified = true;
+    storage.addSecondaryEmail(email, idInfo);
+  }
+
   function completeAddressVerification(completeFunc, token, password, onComplete, onFailure) {
     User.tokenInfo(token, function(info) {
       var invalidInfo = { valid: false };
@@ -123,17 +131,10 @@ BrowserID.User = (function() {
 
           if (valid) {
             result = _.extend({ valid: valid }, info);
-            var email = info.email,
-                idInfo = storage.getEmail(email);
-
             // Now that the address is verified, its verified bit has to be
             // updated as well or else the user will be forced to verify the
             // address again.
-            if (idInfo) {
-              idInfo.verified = true;
-              storage.addEmail(email, idInfo);
-            }
-
+            markAddressVerified(info.email);
             storage.setReturnTo("");
           }
 
@@ -146,7 +147,6 @@ BrowserID.User = (function() {
 
   }
 
-
   function addressVerificationPoll(checkFunc, email, onSuccess, onFailure) {
     function poll() {
       checkFunc(email, function(status) {
@@ -162,6 +162,11 @@ BrowserID.User = (function() {
           // data.
           storage.setReturnTo("");
 
+          // Now that the address is verified, its verified bit has to be
+          // updated as well or else the user will be forced to verify the
+          // address again.
+          markAddressVerified(email);
+
           // To avoid too many address_info requests, returns from each
           // address_info request are cached.  If the user is doing
           // a addressVerificationPoll, it means the user was registering the address
@@ -179,12 +184,22 @@ BrowserID.User = (function() {
           // they just completed a registration.
           registrationComplete = true;
 
-          if (onSuccess) {
-            onSuccess(status);
+          if (status === "complete") {
+            // If the response is complete but the user is not authenticated
+            // to the password level, the user *must* authenticate or else
+            // they will see an error when they try to certify a cert. Users
+            // who have entered their password in this dialog session will be
+            // automatically authenticated in modules/check_registration.js,
+            // all others will have to enter their password. See issue #2088.
+            network.checkAuth(function(authLevel) {
+              if (authLevel !== "password") status = "mustAuth";
+              complete(onSuccess, status);
+            }, onFailure);
           }
+          else complete(onSuccess, status);
         }
         else if (status === 'pending') {
-          pollTimeout = setTimeout(poll, 3000);
+          pollTimeout = setTimeout(poll, pollDuration);
         }
         else if (onFailure) {
             onFailure(status);
@@ -274,12 +289,18 @@ BrowserID.User = (function() {
         provisioning = config.provisioning;
       }
 
+      // BEGIN TESTING API
+      if (config.pollDuration) {
+        pollDuration = config.pollDuration;
+      }
+      // END TESTING API
     },
 
     reset: function() {
       provisioning = BrowserID.Provisioning;
       User.resetCaches();
       registrationComplete = false;
+      pollDuration = POLL_DURATION;
     },
 
     resetCaches: function() {
diff --git a/resources/static/common/js/validation.js b/resources/static/common/js/validation.js
index d49f823719240f0d686f4711406fc7f529b0a821..d64d916cf2748123bbe3ef8bae915e0e5eeec874 100644
--- a/resources/static/common/js/validation.js
+++ b/resources/static/common/js/validation.js
@@ -61,7 +61,7 @@ BrowserID.Validation = (function() {
   }
 
   function passwordLength(password) {
-    var valid = password && (password.length >= 8 && password.length <= 80);
+    var valid = password && (password.length >= bid.PASSWORD_MIN_LENGTH && password.length <= bid.PASSWORD_MAX_LENGTH);
 
     if(!valid) {
       tooltip.showTooltip("#password_length");
diff --git a/resources/static/dialog/css/m.css b/resources/static/dialog/css/m.css
index 6412f9d7c05ee78dd6481afd33d3eba1b9e336b5..bf3a752178c4907763210c46edf9d98fc905e850 100644
--- a/resources/static/dialog/css/m.css
+++ b/resources/static/dialog/css/m.css
@@ -196,5 +196,13 @@
   .inputs > li {
     margin-top: 12px;
   }
+
+  /* The unsupported and cookies_disabled dialogs have to be position: static
+   * or else their content is not displayed on mobile devices. See issue #1998
+   */
+  #error.unsupported, #error.cookies_disabled {
+    position: static;
+    height: 250px;
+  }
 }
 
diff --git a/resources/static/dialog/js/misc/state.js b/resources/static/dialog/js/misc/state.js
index b9f1f7db346016bf81d7b1a735d7f2348112adab..2f87712883cee7008236b049cbb3987a091a2c22 100644
--- a/resources/static/dialog/js/misc/state.js
+++ b/resources/static/dialog/js/misc/state.js
@@ -49,6 +49,10 @@ BrowserID.State = (function() {
       // screen.
       var actionInfo = {
         email: info.email,
+        // password is used to authenticate the user if the verification poll
+        // wsapi comes back with "mustAuth" or the user is currently
+        // authenticated to the "assertion" level. See issue #2088
+        password: self.stagedPassword,
         siteName: self.siteName
       };
 
@@ -56,6 +60,25 @@ BrowserID.State = (function() {
       startAction(actionName, actionInfo);
     }
 
+    function handleEmailConfirmed(msg, info) {
+      self.email = self.stagedEmail;
+
+      if (info.mustAuth) {
+        // If the mustAuth flag comes in, the user has to authenticate.
+        // This is not a cancelable authentication.  mustAuth is set
+        // after a user verifies an address but is not authenticated
+        // to the password level.
+        redirectToState("authenticate_specified_email", {
+          email: self.stagedEmail,
+          mustAuth: info.mustAuth,
+          cancelable: !info.mustAuth
+        });
+      }
+      else {
+        redirectToState("email_chosen", { email: self.stagedEmail });
+      }
+    }
+
 
     handleState("start", function(msg, info) {
       self.hostname = info.hostname;
@@ -120,6 +143,22 @@ BrowserID.State = (function() {
       startAction("doAuthenticate", info);
     });
 
+    handleState("authenticate_specified_email", function(msg, info) {
+      // user must authenticate with their password, kick them over to
+      // the required email screen to enter the password.
+      startAction("doAuthenticateWithRequiredEmail", {
+        email: info.email,
+        secondary_auth: true,
+        cancelable: ("cancelable" in info) ? info.cancelable : true,
+        // This is a user is already authenticated to the assertion
+        // level who has chosen a secondary email address from the
+        // pick_email screen. They would have been shown the
+        // siteTOSPP there.
+        siteTOSPP: false
+      });
+      complete(info.complete);
+    });
+
     handleState("new_user", function(msg, info) {
       self.newUserEmail = info.email;
 
@@ -156,6 +195,11 @@ BrowserID.State = (function() {
        */
       info = _.extend({ email: self.newUserEmail || self.addEmailEmail || self.resetPasswordEmail }, info);
 
+      // stagedPassword is used to authenticate a user if the verification poll
+      // comes back with "mustAuth" or the user is not currently authenticated
+      // to the "password" level.  See issue #2088
+      self.stagedPassword = info.password;
+
       if(self.newUserEmail) {
         self.newUserEmail = null;
         startAction(false, "doStageUser", info);
@@ -172,15 +216,9 @@ BrowserID.State = (function() {
 
     handleState("user_staged", handleEmailStaged.curry("doConfirmUser"));
 
-    handleState("user_confirmed", function() {
-      self.email = self.stagedEmail;
-      redirectToState("email_chosen", { email: self.stagedEmail} );
-    });
+    handleState("user_confirmed", handleEmailConfirmed);
 
-    handleState("staged_address_confirmed", function() {
-      self.email = self.stagedEmail;
-      redirectToState("email_chosen", { email: self.stagedEmail} );
-    });
+    handleState("staged_address_confirmed", handleEmailConfirmed);
 
     handleState("primary_user", function(msg, info) {
       addPrimaryUser = !!info.add;
@@ -320,21 +358,12 @@ BrowserID.State = (function() {
           if (authentication === "assertion") {
              // user must authenticate with their password, kick them over to
             // the required email screen to enter the password.
-            startAction("doAuthenticateWithRequiredEmail", {
-              email: email,
-              secondary_auth: true,
-
-              // This is a user is already authenticated to the assertion
-              // level who has chosen a secondary email address from the
-              // pick_email screen. They would have been shown the
-              // siteTOSPP there.
-              siteTOSPP: false
-            });
+            redirectToState("authenticate_specified_email", info);
           }
           else {
             redirectToState("email_valid_and_ready", info);
+            oncomplete();
           }
-          oncomplete();
         }, oncomplete);
       }
     });
@@ -472,9 +501,7 @@ BrowserID.State = (function() {
 
     handleState("email_staged", handleEmailStaged.curry("doConfirmEmail"));
 
-    handleState("email_confirmed", function() {
-      redirectToState("email_chosen", { email: self.stagedEmail } );
-    });
+    handleState("email_confirmed", handleEmailConfirmed);
 
     handleState("cancel_state", function(msg, info) {
       cancelState(info);
diff --git a/resources/static/dialog/js/modules/check_registration.js b/resources/static/dialog/js/modules/check_registration.js
index 29094a1dcfd4bc7749ed50db4616f3d4baaa46c3..f782037da457ad4b4a40527bf98d68c71c40dd04 100644
--- a/resources/static/dialog/js/modules/check_registration.js
+++ b/resources/static/dialog/js/modules/check_registration.js
@@ -41,7 +41,7 @@ BrowserID.Modules.CheckRegistration = (function() {
         if (status === "complete") {
           // TODO - move the syncEmails somewhere else, perhaps into user.js
           user.syncEmails(function() {
-            self.close(self.verificationMessage);
+            self.close(self.verificationMessage, { mustAuth: false });
             oncomplete && oncomplete();
           });
         }
@@ -49,22 +49,22 @@ BrowserID.Modules.CheckRegistration = (function() {
           // if we have a password (because it was just chosen in dialog),
           // then we can authenticate the user and proceed
           if (self.password) {
+            // XXX Move all of this authentication stuff into user.js.  This
+            // high level shouldn't have to worry about this stuff.
             user.authenticate(self.email, self.password, function (authenticated) {
               if (authenticated) {
                 user.syncEmails(function() {
-                  self.close(self.verificationMessage);
+                  self.close(self.verificationMessage, { mustAuth: false });
                   oncomplete && oncomplete();
                 });
               } else {
-                user.addressInfo(self.email, function(info) {
-                  self.close("authenticate", info);
-                });
+                // unable to log the user in, make them authenticate manually.
+                self.close(self.verificationMessage, { mustAuth: true });
               }
             });
           } else {
-            user.addressInfo(self.email, function(info) {
-              self.close("authenticate", info);
-            });
+            // no password to log the user in, make them authenticate manually.
+            self.close(self.verificationMessage, { mustAuth: true });
           }
 
           oncomplete && oncomplete();
diff --git a/resources/static/dialog/js/modules/required_email.js b/resources/static/dialog/js/modules/required_email.js
index 09d184e5d4d47baccc32527dd25063a1aeefde5e..e950984c3514bb3fc2a5f9c1ef11f97ca1d4c7e6 100644
--- a/resources/static/dialog/js/modules/required_email.js
+++ b/resources/static/dialog/js/modules/required_email.js
@@ -124,7 +124,8 @@ BrowserID.Modules.RequiredEmail = (function() {
           showTemplate({
             signin: true,
             password: auth_level !== "password",
-            secondary_auth: secondaryAuth
+            secondary_auth: secondaryAuth,
+            cancelable: options.cancelable
           });
           ready();
         }
@@ -219,7 +220,8 @@ BrowserID.Modules.RequiredEmail = (function() {
           password: false,
           secondary_auth: false,
           primary: false,
-          personaTOSPP: false
+          personaTOSPP: false,
+          cancelable: true
         }, templateData);
 
         self.renderDialog("required_email", templateData);
diff --git a/resources/static/dialog/views/required_email.ejs b/resources/static/dialog/views/required_email.ejs
index 0bb323a27b3e7478b621349e3b0375a6e69226ee..500327fbfee2c46573f4bf473542d07848b2d8b5 100644
--- a/resources/static/dialog/views/required_email.ejs
+++ b/resources/static/dialog/views/required_email.ejs
@@ -57,7 +57,7 @@
               <button id="verify_address" tabindex="3"><%= gettext("verify email") %></button>
             <% } %>
 
-            <% if (secondary_auth) { %>
+            <% if (cancelable && secondary_auth) { %>
               <a href="#" id="cancel" class="action" tabindex="4"><%= gettext("cancel") %></a>
             <% } %>
           </p>
diff --git a/resources/static/include_js/include.js b/resources/static/include_js/include.js
index 2b173421215b13cfc64079fc43931af2937f550b..f10b3e3887fc440c18e9b37876483e5ad236515c 100644
--- a/resources/static/include_js/include.js
+++ b/resources/static/include_js/include.js
@@ -1108,9 +1108,6 @@
       // don't do duplicative work
       if (commChan) commChan.notify({ method: 'dialog_running' });
 
-      // returnTo is used for post-email-verification redirect
-      if (!options.returnTo) options.returnTo = document.location.pathname;
-
       w = WinChan.open({
         url: ipServer + '/sign_in',
         relay_url: ipServer + '/relay',
@@ -1158,6 +1155,8 @@
           throw new Error("all navigator.id calls must be made on the navigator.id object");
         options = options || {};
         checkCompat(false);
+        // returnTo is used for post-email-verification redirect
+        if (!options.returnTo) options.returnTo = document.location.pathname;
         return internalRequest(options);
       },
       watch: function(options) {
@@ -1179,8 +1178,12 @@
         if (typeof callback === 'function') setTimeout(callback, 0);
       },
       // get an assertion
-      get: function(callback, options) {
-        options = options || {};
+      get: function(callback, passedOptions) {
+        var opts = {};
+        opts.privacyPolicy =  passedOptions.privacyPolicy || undefined;
+        opts.termsOfService = passedOptions.termsOfService || undefined;
+        opts.privacyURL = passedOptions.privacyURL || undefined;
+        opts.tosURL = passedOptions.tosURL || undefined;
         checkCompat(true);
         internalWatch({
           onlogin: function(assertion) {
@@ -1191,17 +1194,17 @@
           },
           onlogout: function() {}
         });
-        options.oncancel = function() {
+        opts.oncancel = function() {
           if (callback) {
             callback(null);
             callback = null;
           }
           observers.login = observers.logout = observers.ready = null;
         };
-        if (options && options.silent) {
+        if (passedOptions && passedOptions.silent) {
           if (callback) setTimeout(function() { callback(null); }, 0);
         } else {
-          internalRequest(options);
+          internalRequest(opts);
         }
       },
       // backwards compatibility with old API
diff --git a/resources/static/pages/js/forgot.js b/resources/static/pages/js/forgot.js
index 5d7c9385355f1dbc1eca1ff0bf85b826b867997e..4d59778a1d04abdd9fff0dd41d580b15f208a985 100644
--- a/resources/static/pages/js/forgot.js
+++ b/resources/static/pages/js/forgot.js
@@ -28,7 +28,7 @@ BrowserID.forgot = (function() {
     if (email && validPass) {
       user.requestPasswordReset(email, pass, function onSuccess(info) {
         if (info.success) {
-          pageHelpers.emailSent(oncomplete);
+          pageHelpers.emailSent("waitForPasswordResetComplete", oncomplete);
         }
         else {
           var tooltipEl = info.reason === "throttle" ? "#could_not_add" : "#not_registered";
diff --git a/resources/static/pages/js/manage_account.js b/resources/static/pages/js/manage_account.js
index 2e650f6a16e9297d2b7fa08a9a681eb45487ac92..6b515f8a60bdf5520990a64cd0d8e3c4b2a524f8 100644
--- a/resources/static/pages/js/manage_account.js
+++ b/resources/static/pages/js/manage_account.js
@@ -49,7 +49,7 @@ BrowserID.manageAccount = (function() {
         displayStoredEmails(oncomplete);
       }
       else if (_.size(emails) > 1) {
-        if (confirmAction(format(gettext("Remove %(email) from your BrowserID?"),
+        if (confirmAction(format(gettext("Remove %(email) from your Persona account?"),
                                  { email: email }))) {
           user.removeEmail(email, function() {
             displayStoredEmails(oncomplete);
@@ -60,7 +60,7 @@ BrowserID.manageAccount = (function() {
         }
       }
       else {
-        if (confirmAction(gettext("Removing the last address will cancel your BrowserID account.\nAre you sure you want to continue?"))) {
+        if (confirmAction(gettext("Removing the last address will cancel your Persona account.\nAre you sure you want to continue?"))) {
           user.cancelUser(function() {
             doc.location="/";
             complete();
@@ -93,7 +93,7 @@ BrowserID.manageAccount = (function() {
   }
 
   function cancelAccount(oncomplete) {
-    if (confirmAction(gettext("Are you sure you want to cancel your BrowserID account?"))) {
+    if (confirmAction(gettext("Are you sure you want to cancel your Persona account?"))) {
       user.cancelUser(function() {
         doc.location="/";
         oncomplete && oncomplete();
@@ -139,6 +139,13 @@ BrowserID.manageAccount = (function() {
       tooltip.showTooltip("#tooltipOldRequired");
       complete(false);
     }
+    else if(oldPassword.length < bid.PASSWORD_MIN_LENGTH || bid.PASSWORD_MAX_LENGTH < oldPassword.length) {
+      // If the old password is out of range, we know it is invalid. Show the
+      // tooltip. See issue #2121
+      // - https://github.com/mozilla/browserid/issues/2121
+      tooltip.showTooltip("#tooltipInvalidPassword");
+      complete(false);
+    }
     else if(!newPassword) {
       tooltip.showTooltip("#tooltipNewRequired");
       complete(false);
@@ -147,7 +154,7 @@ BrowserID.manageAccount = (function() {
       tooltip.showTooltip("#tooltipPasswordsSame");
       complete(false);
     }
-    else if(newPassword.length < 8 || 80 < newPassword.length) {
+    else if(newPassword.length < bid.PASSWORD_MIN_LENGTH || bid.PASSWORD_MAX_LENGTH < newPassword.length) {
       tooltip.showTooltip("#tooltipPasswordLength");
       complete(false);
     }
diff --git a/resources/static/pages/js/page_helpers.js b/resources/static/pages/js/page_helpers.js
index 9ec528c6b0103bdf4ebbfbb7bde50f75eb7cb139..c43ed717f48fe3b7bdb6ac107a81b6a0c565429b 100644
--- a/resources/static/pages/js/page_helpers.js
+++ b/resources/static/pages/js/page_helpers.js
@@ -68,7 +68,7 @@ BrowserID.PageHelpers = (function() {
   function getFailure(error, callback) {
     return function onFailure(info) {
       showFailure(error, info, callback);
-    }
+    };
   }
 
   function replaceFormWithNotice(selector, onComplete) {
@@ -88,7 +88,7 @@ BrowserID.PageHelpers = (function() {
       .promise().done(onComplete);
   }
 
-  function emailSent(onComplete) {
+  function emailSent(pollFuncName, onComplete) {
     origStoredEmail = getStoredEmail();
     dom.setInner('#sentToEmail', origStoredEmail);
 
@@ -96,7 +96,7 @@ BrowserID.PageHelpers = (function() {
 
     replaceInputsWithNotice(".emailsent");
 
-    user.waitForUserValidation(origStoredEmail, function(status) {
+    user[pollFuncName](origStoredEmail, function(status) {
       userValidationComplete(status);
     });
     onComplete && onComplete();
@@ -124,7 +124,7 @@ BrowserID.PageHelpers = (function() {
 
   function openPrimaryAuth(winchan, email, baseURL, callback) {
     if(!(email && baseURL)) {
-      throw "cannot verify with primary without an email address and URL"
+      throw "cannot verify with primary without an email address and URL";
     }
 
     winchan.open({
diff --git a/resources/static/pages/js/signup.js b/resources/static/pages/js/signup.js
index 49677c8e48721f6fb75f3e2a14726716569617be..1284ebf88418cac8dff4d94fbeeb705a0830f7c4 100644
--- a/resources/static/pages/js/signup.js
+++ b/resources/static/pages/js/signup.js
@@ -81,7 +81,7 @@ BrowserID.signUp = (function() {
       if(valid) {
         user.createSecondaryUser(this.emailToStage, pass, function(status) {
           if(status.success) {
-            pageHelpers.emailSent(oncomplete && oncomplete.curry(true));
+            pageHelpers.emailSent("waitForUserValidation", oncomplete && oncomplete.curry(true));
           }
           else {
             tooltip.showTooltip("#could_not_add");
diff --git a/resources/static/pages/js/verify_secondary_address.js b/resources/static/pages/js/verify_secondary_address.js
index ee9e2227c2bb35be670dd0962599f849208360b2..793538d61710ee4d6a523defa73010443f9e3157 100644
--- a/resources/static/pages/js/verify_secondary_address.js
+++ b/resources/static/pages/js/verify_secondary_address.js
@@ -119,6 +119,7 @@ BrowserID.verifySecondaryAddress = (function() {
           // These are users who are authenticating in a different browser or
           // session than the initiator.
           dom.addClass("body", "enter_password");
+          dom.focus("input[autofocus]");
           complete(oncomplete, true);
         }
         else {
diff --git a/resources/static/test/cases/common/js/network.js b/resources/static/test/cases/common/js/network.js
index 7000cf007a253c8277d90095f023326bef838fa4..fc153cacbf2c62429521c6f4c23e8fb99762756a 100644
--- a/resources/static/test/cases/common/js/network.js
+++ b/resources/static/test/cases/common/js/network.js
@@ -55,10 +55,7 @@
       transport.useResult("complete");
       network[funcName]("registered@testuser.com", function(status) {
         equal(status, "complete");
-        network.checkAuth(function(auth_level) {
-          equal(auth_level, "password", "user can only be authenticated to password level after verification is complete");
-          start();
-        });
+        start();
       }, testHelpers.unexpectedFailure);
     });
   }
@@ -257,53 +254,11 @@
     failureCheck(network.createUser, "validuser", "password", "origin");
   });
 
-  asyncTest("checkUserRegistration returns pending - pending status, user is not logged in", function() {
-    transport.useResult("pending");
-
-    // To properly check the user registration status, we first have to
-    // simulate the first checkAuth or else network has no context from which
-    // to work.
-    network.checkAuth(function(auth_status) {
-      equal(!!auth_status, false, "user not yet authenticated");
-      network.checkUserRegistration("registered@testuser.com", function(status) {
-        equal(status, "pending");
-        network.checkAuth(function(auth_status) {
-          equal(!!auth_status, false, "user not yet authenticated");
-          start();
-        }, testHelpers.unexpectedFailure);
-      }, testHelpers.unexpectedFailure);
-    }, testHelpers.unexpectedFailure);
-  });
+  asyncTest("checkUserRegistration returns pending - pending status, user is not logged in", testVerificationPending.curry("checkUserRegistration"));
 
-  asyncTest("checkUserRegistration returns mustAuth - mustAuth status, user is not logged in", function() {
-    transport.useResult("mustAuth");
+  asyncTest("checkUserRegistration returns mustAuth - mustAuth status, user is not logged in", testVerificationMustAuth.curry("checkUserRegistration"));
 
-    network.checkAuth(function(auth_status) {
-      equal(!!auth_status, false, "user not yet authenticated");
-      network.checkUserRegistration("registered@testuser.com", function(status) {
-        equal(status, "mustAuth");
-        network.checkAuth(function(auth_status) {
-          equal(!!auth_status, false, "user not yet authenticated");
-          start();
-        }, testHelpers.unexpectedFailure);
-      }, testHelpers.unexpectedFailure);
-    }, testHelpers.unexpectedFailure);
-  });
-
-  asyncTest("checkUserRegistration returns complete - complete status, user is logged in", function() {
-    transport.useResult("complete");
-
-    network.checkAuth(function(auth_status) {
-      equal(!!auth_status, false, "user not yet authenticated");
-      network.checkUserRegistration("registered@testuser.com", function(status) {
-        equal(status, "complete");
-        network.checkAuth(function(auth_status) {
-          equal(auth_status, "password", "user authenticated after checkUserRegistration returns complete");
-          start();
-        }, testHelpers.unexpectedFailure);
-      }, testHelpers.unexpectedFailure);
-    }, testHelpers.unexpectedFailure);
-  });
+  asyncTest("checkUserRegistration returns complete - complete status, user is logged in", testVerificationComplete.curry("checkUserRegistration"));
 
   asyncTest("checkUserRegistration with XHR failure", function() {
     failureCheck(network.checkUserRegistration, "registered@testuser.com");
diff --git a/resources/static/test/cases/common/js/user.js b/resources/static/test/cases/common/js/user.js
index f39ed418c20b87b8fd73f881e1f965cfcfb44aeb..46f7a2376315a1cc18af304fd5f0d8b429cd48b0 100644
--- a/resources/static/test/cases/common/js/user.js
+++ b/resources/static/test/cases/common/js/user.js
@@ -320,13 +320,16 @@
     );
   });
 
-  asyncTest("waitForUserValidation with `complete` response", function() {
+  asyncTest("waitForUserValidation with complete from backend, user not authed - `mustAuth` response", function() {
     storage.setReturnTo(testOrigin);
 
+    xhr.setContextInfo("auth_level", false);
     xhr.useResult("complete");
 
     lib.waitForUserValidation("registered@testuser.com", function(status) {
-      equal(status, "complete", "complete response expected");
+      equal(status, "mustAuth", "mustAuth response expected");
+
+      testHelpers.testEmailMarkedVerified("registered@testuser.com");
 
       ok(!storage.getReturnTo(), "staged on behalf of is cleared when validation completes");
       start();
@@ -341,6 +344,8 @@
     lib.waitForUserValidation("registered@testuser.com", function(status) {
       equal(status, "mustAuth", "mustAuth response expected");
 
+      testHelpers.testEmailMarkedVerified("registered@testuser.com");
+
       ok(!storage.getReturnTo(), "staged on behalf of is cleared when validation completes");
       start();
     }, testHelpers.unexpectedXHRFailure);
@@ -844,12 +849,28 @@
   });
 
 
- asyncTest("waitForEmailValidation `complete` response", function() {
+ asyncTest("waitForEmailValidation with `complete` backend response, user authenticated to assertion level - expect 'mustAuth'", function() {
+    storage.setReturnTo(testOrigin);
+    xhr.setContextInfo("auth_level", "assertion");
+
+    xhr.useResult("complete");
+    lib.waitForEmailValidation("registered@testuser.com", function(status) {
+      ok(!storage.getReturnTo(), "staged on behalf of is cleared when validation completes");
+      testHelpers.testEmailMarkedVerified("registered@testuser.com");
+      equal(status, "mustAuth", "mustAuth response expected");
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+
+ asyncTest("waitForEmailValidation with `complete` backend response, user authenticated to password level - expect 'complete'", function() {
     storage.setReturnTo(testOrigin);
+    xhr.setContextInfo("auth_level", "password");
 
     xhr.useResult("complete");
     lib.waitForEmailValidation("registered@testuser.com", function(status) {
       ok(!storage.getReturnTo(), "staged on behalf of is cleared when validation completes");
+      testHelpers.testEmailMarkedVerified("registered@testuser.com");
       equal(status, "complete", "complete response expected");
       start();
     }, testHelpers.unexpectedXHRFailure);
@@ -861,6 +882,7 @@
 
     lib.waitForEmailValidation("registered@testuser.com", function(status) {
       ok(!storage.getReturnTo(), "staged on behalf of is cleared when validation completes");
+      testHelpers.testEmailMarkedVerified("registered@testuser.com");
       equal(status, "mustAuth", "mustAuth response expected");
       start();
     }, testHelpers.unexpectedXHRFailure);
diff --git a/resources/static/test/cases/common/js/validation.js b/resources/static/test/cases/common/js/validation.js
index 228c61fc38d7cb7c61b18f4787d4bdddb7ac57d3..271d6b5285c3bd20347628ae17f47354f75ed95b 100644
--- a/resources/static/test/cases/common/js/validation.js
+++ b/resources/static/test/cases/common/js/validation.js
@@ -8,6 +8,7 @@
 
   var bid = BrowserID,
       validation = bid.Validation,
+      testHelpers = bid.TestHelpers,
       tooltipShown,
       origShowTooltip;
 
@@ -205,21 +206,19 @@
 
 
   test("passwordAndValidationPassword with too short password", function() {
-    var valid = validation.passwordAndValidationPassword("pass", "password");
+    var tooShort = testHelpers.generateString(bid.PASSWORD_MIN_LENGTH - 1);
+    var valid = validation.passwordAndValidationPassword(tooShort, tooShort);
 
     equal(valid, false, "too short password is invalid");
     equal(tooltipShown, true, "too short password shows tooltip");
   });
 
   test("passwordAndValidationPassword with too long password", function() {
-    var tooLong = "";
-    for(var i = 0; i < 81; i++) {
-      tooLong += (i % 10);
-    }
+    var tooLong = testHelpers.generateString(bid.PASSWORD_MAX_LENGTH + 1);
     var valid = validation.passwordAndValidationPassword(tooLong, tooLong);
 
-    equal(valid, false, "too short password is invalid");
-    equal(tooltipShown, true, "too short password shows tooltip");
+    equal(valid, false, "too long password is invalid");
+    equal(tooltipShown, true, "too long password shows tooltip");
   });
 
   test("passwordAndValidationPassword with empty validation password", function() {
diff --git a/resources/static/test/cases/dialog/js/misc/state.js b/resources/static/test/cases/dialog/js/misc/state.js
index eaec7deb5cc6a0dd5583b7448d34a5969fe930f8..8fda485c5018cb0fd0febbd4133a6ffb4f118d91 100644
--- a/resources/static/test/cases/dialog/js/misc/state.js
+++ b/resources/static/test/cases/dialog/js/misc/state.js
@@ -568,6 +568,33 @@
     mediator.publish("window_unload");
   });
 
+  function testAuthenticateSpecifiedEmail(specified, expected) {
+    var options = {
+      email: TEST_EMAIL,
+      complete: function() {
+        testActionStarted("doAuthenticateWithRequiredEmail", {
+          cancelable: expected
+        });
+        start();
+      }
+    };
+
+    if (typeof specified !== "undefined") options.cancelable = specified;
+
+    mediator.publish("authenticate_specified_email", options);
+  }
+
+  asyncTest("authenticate_specified_email with false specified - call doAuthenticateWithRequiredEmail using specified cancelable", function() {
+    testAuthenticateSpecifiedEmail(false, false);
+  });
+
+  asyncTest("authenticate_specified_email with true specified - call doAuthenticateWithRequiredEmail using specified cancelable", function() {
+    testAuthenticateSpecifiedEmail(true, true);
+  });
+
+  asyncTest("authenticate_specified_email without cancelable - call doAuthenticateWithRequiredEmail, cancelable defaults to true", function() {
+    testAuthenticateSpecifiedEmail(undefined, true);
+  });
 
 
 }());
diff --git a/resources/static/test/cases/dialog/js/modules/check_registration.js b/resources/static/test/cases/dialog/js/modules/check_registration.js
index 29c84236e7e2f0c9cf52801cfd678c04882145dc..e7499d31eb145d214e40743adbb3858c9d820a2b 100644
--- a/resources/static/test/cases/dialog/js/modules/check_registration.js
+++ b/resources/static/test/cases/dialog/js/modules/check_registration.js
@@ -8,15 +8,17 @@
 
   var controller,
       bid = BrowserID,
+      user = bid.User,
       xhr = bid.Mocks.xhr,
       network = bid.Network,
       testHelpers = bid.TestHelpers,
       register = testHelpers.register;
 
-  function createController(verifier, message, required) {
+  function createController(verifier, message, required, password) {
     controller = bid.Modules.CheckRegistration.create();
     controller.start({
       email: "registered@testuser.com",
+      password: password,
       verifier: verifier,
       verificationMessage: message,
       required: required,
@@ -40,37 +42,52 @@
     }
   });
 
-  function testVerifiedUserEvent(event_name, message) {
-    createController("waitForUserValidation", event_name);
-    register(event_name, function() {
-      ok(true, message);
+  function testVerifiedUserEvent(event_name, message, password) {
+    createController("waitForUserValidation", event_name, false, password);
+    register(event_name, function(msg, info) {
+      equal(info.mustAuth, false, "user does not need to verify");
       start();
     });
     controller.startCheck();
   }
 
-  asyncTest("user validation with mustAuth result - callback with email, type and known set to true", function() {
-    xhr.useResult("mustAuth");
-    createController("waitForUserValidation");
-    register("authenticate", function(msg, info) {
-      // we want the email, type and known all sent back to the caller so that
-      // this information does not need to be queried again.
-      equal(info.email, "registered@testuser.com", "correct email");
-      ok(info.type, "type sent with info");
-      ok(info.known, "email is known");
+  function testMustAuthUserEvent(event_name, message) {
+    createController("waitForUserValidation", event_name);
+    register(event_name, function(msg, info) {
+      equal(info.mustAuth, true, "user needs to verify");
       start();
     });
     controller.startCheck();
+  }
+
+  asyncTest("user validation with mustAuth result - userVerified with mustAuth: true", function() {
+    xhr.useResult("mustAuth");
+    testMustAuthUserEvent("user_verified");
+  });
+
+  asyncTest("user validation with pending->complete with auth_level = assertion, no authentication info given - user_verified with mustAuth triggered", function() {
+    user.init({ pollDuration: 100 });
+    xhr.useResult("pending");
+    xhr.setContextInfo("auth_level", "assertion");
+    testMustAuthUserEvent("user_verified");
+
+    // use setTimeout to simulate a delay in the user opening the email.
+    setTimeout(function() {
+      xhr.useResult("complete");
+    }, 50);
   });
 
-  asyncTest("user validation with pending->complete result ~3 seconds", function() {
+  asyncTest("user validation with pending->complete with auth_level = password - user_verified triggered", function() {
+    user.init({ pollDuration: 100 });
     xhr.useResult("pending");
+    xhr.setContextInfo("auth_level", "password");
+
+    testVerifiedUserEvent("user_verified");
 
-    testVerifiedUserEvent("user_verified", "User verified");
     // use setTimeout to simulate a delay in the user opening the email.
     setTimeout(function() {
       xhr.useResult("complete");
-    }, 500);
+    }, 50);
   });
 
   asyncTest("user validation with XHR error - show error message", function() {
diff --git a/resources/static/test/cases/pages/js/forgot.js b/resources/static/test/cases/pages/js/forgot.js
index 67746a327b96ea7296f790c9af9c4ff0ed51e8b3..d6179186f4f6b1eb5cf421e2ad56e4d6ec12e09b 100644
--- a/resources/static/test/cases/pages/js/forgot.js
+++ b/resources/static/test/cases/pages/js/forgot.js
@@ -72,14 +72,14 @@
 
   asyncTest("requestPasswordReset with too short of a password", function() {
     $("#email").val("unregistered@testuser.com");
-    $("#password,#vpassword").val("fail");
+    $("#password,#vpassword").val(testHelpers.generateString(bid.PASSWORD_MIN_LENGTH - 1));
 
     testEmailNotSent();
   });
 
   asyncTest("requestPasswordReset with too long of a password", function() {
     $("#email").val("unregistered@testuser.com");
-    $("#password,#vpassword").val(testHelpers.generateString(81));
+    $("#password,#vpassword").val(testHelpers.generateString(bid.PASSWORD_MAX_LENGTH + 1));
 
     testEmailNotSent();
   });
diff --git a/resources/static/test/cases/pages/js/manage_account.js b/resources/static/test/cases/pages/js/manage_account.js
index a7c5518668640fcadd735a2b3d204e258bbfd654..2b5caff3e100f3cb8d860c2bbe900ccd43059585 100644
--- a/resources/static/test/cases/pages/js/manage_account.js
+++ b/resources/static/test/cases/pages/js/manage_account.js
@@ -1,5 +1,5 @@
 /*jshint browser: true, forin: true, laxbreak: true */
-/*global test: true, start: true, module: true, ok: true, equal: true, BrowserID:true */
+/*global test: true, start: true, module: true, ok: true, equal: true, BrowserID:true, notEqual: 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/. */
@@ -9,8 +9,10 @@
   var bid = BrowserID,
       xhr = bid.Mocks.xhr,
       errorScreen = bid.Screens.error,
+      network = bid.Network,
       storage = bid.Storage,
       testHelpers = bid.TestHelpers,
+      generateString = testHelpers.generateString,
       tooltip = bid.Tooltip,
       mocks = {
         confirm: function() { return true; },
@@ -29,6 +31,37 @@
     }
   });
 
+  function testPasswordChangeSuccess(oldPass, newPass, msg) {
+    testPasswordChange(oldPass, newPass, function(status) {
+      equal(status, true, msg);
+      // if success is expected, both password fields should be visible.
+      equal($("#old_password").val(), "", "old_password field is cleared");
+      equal($("#new_password").val(), "", "new_password field is cleared");
+      testHelpers.testTooltipNotVisible();
+      network.checkAuth(function(authLevel) {
+        equal(authLevel, "password", "after password change, user authenticated to password level");
+        start();
+      }, testHelpers.unexpectedXHRFailure);
+    }, msg);
+  }
+
+  function testPasswordChangeFailure(oldPass, newPass, msg) {
+    testPasswordChange(oldPass, newPass, function(status) {
+      equal(status, false, msg);
+      testHelpers.testTooltipVisible();
+      start();
+    }, msg);
+  }
+
+  function testPasswordChange(oldPass, newPass, testStrategy, msg) {
+    bid.manageAccount(mocks, function() {
+      $("#old_password").val(oldPass);
+      $("#new_password").val(newPass);
+
+      bid.manageAccount.changePassword(testStrategy);
+    });
+  }
+
   asyncTest("no email addresses are displayed if there are no children", function() {
     xhr.useResult("no_identities");
 
@@ -96,7 +129,7 @@
       });
     });
   });
-  
+
   asyncTest("removeEmail doesn't cancel the account when removing a non-existent e-mail", function() {
     bid.manageAccount(mocks, function() {
       bid.manageAccount.removeEmail("non@existent.com", function() {
@@ -105,7 +138,7 @@
       });
     });
   });
-  
+
   asyncTest("removeEmail doesn't cancel the account when out of sync with the server", function() {
     bid.manageAccount(mocks, function() {
       xhr.useResult("multiple");
@@ -178,84 +211,32 @@
   });
 
   asyncTest("changePassword with missing old password - tooltip", function() {
-    bid.manageAccount(mocks, function() {
-      $("#old_password").val("");
-      $("#new_password").val("newpassword");
-
-      bid.manageAccount.changePassword(function(status) {
-        equal(status, false, "on missing old password, status is false");
-        testHelpers.testTooltipVisible();
-        start();
-      });
-    });
+    testPasswordChangeFailure("", "newpassword", "missing old password, expected failure");
   });
 
-  asyncTest("changePassword with missing new password - tooltip", function() {
-    bid.manageAccount(mocks, function() {
-      $("#old_password").val("oldpassword");
-      $("#new_password").val("");
-
-      bid.manageAccount.changePassword(function(status) {
-        equal(status, false, "on missing new password, status is false");
-        testHelpers.testTooltipVisible();
-        start();
-      });
-    });
+  asyncTest("changePassword with too short of an old password - tooltip", function() {
+    testPasswordChangeFailure(generateString(bid.PASSWORD_MIN_LENGTH - 1), "newpassword", "missing old password, expected failure");
   });
 
-  asyncTest("changePassword with too short of a password - tooltip", function() {
-    bid.manageAccount(mocks, function() {
-      $("#old_password").val("oldpassword");
-      $("#new_password").val("pass");
-
-      bid.manageAccount.changePassword(function(status) {
-        equal(status, false, "on too short of a password, status is false");
-        testHelpers.testTooltipVisible();
-        start();
-      });
-    });
+  asyncTest("changePassword with too long of an old password - tooltip", function() {
+    testPasswordChangeFailure(generateString(bid.PASSWORD_MAX_LENGTH + 1), "newpassword", "missing old password, expected failure");
   });
 
-  asyncTest("changePassword with too long of a password - tooltip", function() {
-    bid.manageAccount(mocks, function() {
-      $("#old_password").val("oldpassword");
-      $("#new_password").val(testHelpers.generateString(81));
-
-      bid.manageAccount.changePassword(function(status) {
-        equal(status, false, "on too long of a password, status is false");
-        testHelpers.testTooltipVisible();
-        start();
-      });
-    });
+  asyncTest("changePassword with missing new password - tooltip", function() {
+    testPasswordChangeFailure("oldpassword", "", "missing new password, expected failure");
   });
 
+  asyncTest("changePassword with too short of a new password - tooltip", function() {
+    testPasswordChangeFailure("oldpassword", generateString(bid.PASSWORD_MIN_LENGTH - 1), "too short new password, expected failure");
+  });
 
-  asyncTest("changePassword with incorrect old password - tooltip", function() {
-    bid.manageAccount(mocks, function() {
-      xhr.useResult("incorrectPassword");
-
-      $("#old_password").val("incorrectpassword");
-      $("#new_password").val("newpassword");
-
-      bid.manageAccount.changePassword(function(status) {
-        equal(status, false, "on incorrect old password, status is false");
-        testHelpers.testTooltipVisible();
-        start();
-      });
-    });
+  asyncTest("changePassword with too long of a new password - tooltip", function() {
+    testPasswordChangeFailure("oldpassword", generateString(bid.PASSWORD_MAN_LENGTH + 1), "too short new password, expected failure");
   });
 
-  asyncTest("changePassword with same old and new password - tooltip", function() {
-    bid.manageAccount(mocks, function() {
-      $("#old_password").val("password");
-      $("#new_password").val("password");
 
-      bid.manageAccount.changePassword(function(status) {
-        equal(status, false, "do not update when old and new passwords are the same");
-        testHelpers.testTooltipVisible();
-        start();
-      });
-    });
+  asyncTest("changePassword with same old and new password - tooltip", function() {
+    testPasswordChangeFailure("password", "password", "password same, expected failure");
   });
 
   asyncTest("changePassword with XHR error - error message", function() {
@@ -272,52 +253,29 @@
     });
   });
 
-  asyncTest("changePassword with user authenticated to password level, happy case", function() {
-    bid.manageAccount(mocks, function() {
-      $("#old_password").val("oldpassword");
-      $("#new_password").val("newpassword");
-
-      bid.manageAccount.changePassword(function(status) {
-        equal(status, true, "on proper completion, status is true");
-        equal(tooltip.shown, false, "on proper completion, tooltip is not shown");
-
-        equal($("#old_password").val(), "", "old_password field is cleared");
-        equal($("#new_password").val(), "", "new_password field is cleared");
-
-        start();
-      });
-    });
+  asyncTest("changePassword with user authenticated to password level, incorrect old password - tooltip", function() {
+    xhr.setContextInfo("auth_level", "password");
+    xhr.useResult("incorrectPassword");
+    testPasswordChangeFailure("incorrectpassword", "newpassword", "incorrect old password, expected failure");
   });
 
-  asyncTest("changePassword with user authenticated to assertion level level, incorrect password - show tooltip", function() {
+  asyncTest("changePassword with user authenticated to assertion level, incorrect password - show tooltip", function() {
     xhr.setContextInfo("auth_level", "assertion");
+    xhr.useResult("incorrectPassword");
 
-    bid.manageAccount(mocks, function() {
-      $("#old_password").val("oldpassword");
-      $("#new_password").val("newpassword");
-      xhr.useResult("incorrectPassword");
+    testPasswordChangeFailure("oldpassword", "newpassword", "incorrect old password, expected failure");
+  });
 
-      bid.manageAccount.changePassword(function(status) {
-        equal(status, false, "bad password, status is false");
-        testHelpers.testTooltipVisible();
-        start();
-      });
-    });
+  asyncTest("changePassword with user authenticated to password level, happy case", function() {
+    xhr.setContextInfo("auth_level", "password");
+
+    testPasswordChangeSuccess("oldpassword", "newpassword", "proper completion, no need to authenticate");
   });
 
   asyncTest("changePassword with user authenticated to assertion level level, correct password - log user in, change password", function() {
     xhr.setContextInfo("auth_level", "assertion");
 
-    bid.manageAccount(mocks, function() {
-      $("#old_password").val("oldpassword");
-      $("#new_password").val("newpassword");
-
-      bid.manageAccount.changePassword(function(status) {
-        equal(status, true, "on proper completion, status is true");
-        equal(tooltip.shown, false, "on proper completion, tooltip is not shown");
-        start();
-      });
-    });
+    testPasswordChangeSuccess("oldpassword", "newpassword", "proper completion after authenticating user");
   });
 
 }());
diff --git a/resources/static/test/cases/pages/js/page_helpers.js b/resources/static/test/cases/pages/js/page_helpers.js
index 40b8b22f17bd5c27681ec8c4fe1f586c438e4279..b389cbc506b61e717324c33c7a5f6812b16b2f20 100644
--- a/resources/static/test/cases/pages/js/page_helpers.js
+++ b/resources/static/test/cases/pages/js/page_helpers.js
@@ -99,7 +99,7 @@
     // below.
     xhr.useResult("complete");
 
-    pageHelpers.emailSent(function() {
+    pageHelpers.emailSent("waitForUserValidation", function() {
       equal($("#sentToEmail").html(), "registered@testuser.com", "correct email is set");
       equal($(".emailsent").is(":visible"), true, "emailsent is visible");
       equal($(".forminputs").is(":visible"), false, "inputs are hidden");
@@ -135,7 +135,7 @@
   asyncTest("cancelEmailSent restores the stored email, inputs are shown again", function() {
     pageHelpers.setStoredEmail("registered@testuser.com");
     xhr.useResult("complete");
-    pageHelpers.emailSent(function() {
+    pageHelpers.emailSent("waitForUserValidation", function() {
       pageHelpers.cancelEmailSent(function() {
         var email = pageHelpers.getStoredEmail();
         equal(email, "registered@testuser.com", "stored email is reset on cancel");
diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js
index 21416158407c31224d7590210ea219dc38585f89..ce7ee117468a1af34dfd8827c6adc60f611d12f3 100644
--- a/resources/static/test/testHelpers/helpers.js
+++ b/resources/static/test/testHelpers/helpers.js
@@ -197,6 +197,10 @@ BrowserID.TestHelpers = (function() {
       equal(tooltip.shown, true, "tooltip is visible");
     },
 
+    testTooltipNotVisible: function() {
+      equal(tooltip.shown, false, "tooltip is not visible");
+    },
+
     failureCheck: function failureCheck(cb) {
       // Take the original arguments, take off the function.  Add any additional
       // arguments that were passed in, and then tack on the onSuccess and
@@ -320,6 +324,12 @@ BrowserID.TestHelpers = (function() {
         }
         input.remove();
       }
+    },
+
+    testEmailMarkedVerified: function(email, msg) {
+      var emailInfo = storage.getEmail(email);
+      equal(emailInfo && emailInfo.verified, true,
+        "verified bit set for " + email);
     }
   };
 
diff --git a/resources/views/about.ejs b/resources/views/about.ejs
index 43da5ce79b07edc8eae6cfcfa747172642429571..ce1ee940a016969c1bba1a1a84caf5a1fd66475b 100644
--- a/resources/views/about.ejs
+++ b/resources/views/about.ejs
@@ -9,7 +9,13 @@
             <article class="blurb">
                 <div class="info first">
                     <h1><%- gettext('Persona replaces multiple passwords') %></h1>
-                    <p><%- gettext('Sites such as <a href="http://crossword.thetimes.co.uk/" target="_blank">The Times Crossword</a>, <a href="http://current.openphoto.me/" target="_blank">OpenPhoto</a> and <a href="https://www.voo.st/" target="_blank">Voost</a> use Persona instead of usernames to sign you in.') %></p><p><%- gettext('This means you only need one password to sign in to many sites.') %></p>
+                    <p><%- format(gettext('Sites such as <a %(timesLink)>The Times Crossword</a>, <a %(openphotoLink)>OpenPhoto</a> and <a %(voostLink)>Voost</a> use Persona instead of usernames to sign you in.'),
+                                  {
+                                    timesLink: 'href="http://crossword.thetimes.co.uk/" target="_blank"',
+                                    openphotoLink: 'href="http://current.openphoto.me/" target="_blank"',
+                                    voostLink: 'href="https://www.voo.st/" target="_blank"'
+                                  })
+                           %></p><p><%- gettext('This means you only need one password to sign in to many sites.') %></p>
                 </div>
 
                 <div class="graphic">
@@ -42,7 +48,7 @@
             </article>
         </section>
 
-        <a href="https://developer.mozilla.org/en/BrowserID/Quick_Setup" class="developers" target="_blank"><img src="<%- cachify('/pages/i/developers-link.png') %>" alt="<%- gettext('Persona for developers') %>"><span><%- gettext('Implement Persona on your site') %> </span><%- gettext('Developer guides and API documentation') %></a>
+        <a href="https://developer.mozilla.org/en/BrowserID" class="developers" target="_blank"><img src="<%- cachify('/pages/i/developers-link.png') %>" alt="<%- gettext('Persona for developers') %>"><span><%- gettext('Implement Persona on your site') %> </span><%- gettext('Developer guides and API documentation') %></a>
     </div><!-- #dashboard -->
 </div>
 
diff --git a/resources/views/forgot.ejs b/resources/views/forgot.ejs
index 59a0c5d591a27cfb5768b6e1ac5ccb0ed1d1ec93..31ce339a41ebccdd2fd5934c966815d880476041 100644
--- a/resources/views/forgot.ejs
+++ b/resources/views/forgot.ejs
@@ -6,13 +6,13 @@
     <div id="vAlign">
         <!-- XXX this form submits to nowhere -->
         <form id="signUpForm" class="cf authform" novalidate>
-            <h1><%- gettext('Forgot Password') %></h1>
+            <h1><%- gettext('Reset your password') %></h1>
             <div class="notifications">
                 <div class="notification emailsent">
                   <h2><%- gettext('Confirm your email address') %></h2>
 
                   <p>
-                    <%- gettext('Check your email at <strong id="sentToEmail"></strong>.') %>
+                    <%- format(gettext('Check your email at <strong %(checkId)></strong>.'), { checkId: 'id="sentToEmail"' }) %>
                   </p>
 
                   <p>
@@ -44,8 +44,8 @@
                 </li>
 
                 <li>
-                    <label for="password"><%- gettext('Password') %></label>
-                    <input id="password" placeholder="<%- gettext('Password') %>" type="password" maxlength="80">
+                    <label for="password"><%= format(gettext("Create a new password to use with %s."), ["Persona"]) %></label>
+                    <input id="password" placeholder="<%- gettext('Password') %>" type="password" maxlength="80" />
 
                     <div id="password_required" class="tooltip" for="password">
                         <%- gettext('Password is required.') %>
diff --git a/resources/views/index.ejs b/resources/views/index.ejs
index 98c47d935442bea0271471800a9297be03b1ab45..c1dcdf8c9756576bce2888d6d71b0a06543ff563 100644
--- a/resources/views/index.ejs
+++ b/resources/views/index.ejs
@@ -29,7 +29,7 @@
   <script type="text/html" id="templateManage">
     <div id="content">
         <div class="newsbanner" id="newuser">
-          <%- gettext('New to Mozilla Persona? <a href="/about">Learn more</a>') %>
+          <%- format(gettext('New to Mozilla Persona? <a %(learnMoreLink)>Learn more</a>'), { learnMoreLink: 'href="/about"' }) %>
         </div>
 
         <div id="manage">
@@ -74,7 +74,7 @@
             </section>
 
 
-            <p id="disclaimer"><%- gettext('You may, at any time, <a href="#" id="cancelAccount" class="action">cancel your account</a>') %></p>
+            <p id="disclaimer"><%- format(gettext('You may, at any time, <a %(cancelLink)>cancel your account</a>'), { cancelLink: 'href="#" id="cancelAccount" class="action"' }) %></p>
         </div>
     </div>
 
diff --git a/resources/views/layout.ejs b/resources/views/layout.ejs
index 3ebad475ee2b3f45c290b1d8c41ec650ac699d19..f2064cc20adc88826983716768332d73107c4fe3 100644
--- a/resources/views/layout.ejs
+++ b/resources/views/layout.ejs
@@ -30,7 +30,7 @@
 
         <ul class="nav cf">
             <li><a href="/about"><%= gettext("How it works") %></a></li>
-            <li><a href="https://developer.mozilla.org/en/BrowserID/Quick_Setup" target="_blank"><%= gettext("Developers") %></a></li>
+            <li><a href="https://developer.mozilla.org/en/BrowserID" target="_blank"><%= gettext("Developers") %></a></li>
 
             <li class="signIn"><a class="signIn" href="/signin"><%= gettext("Sign In") %></a></li>
             <li class="signOut"><a class="signOut" href="/"><%= gettext("Sign Out") %></a></li>
diff --git a/resources/views/signin.ejs b/resources/views/signin.ejs
index 683af56d65753361e8a228c85fec202bd3111e51..7717ff0279afa8f5cfbbe5db5f308258c5a8c5b3 100644
--- a/resources/views/signin.ejs
+++ b/resources/views/signin.ejs
@@ -10,7 +10,8 @@
 
             <ul class="notifications">
                 <li class="notification" id="unknown_secondary">
-                  <%- gettext('<strong id="unknown_email">Email</strong> is not registered. Would you like to <a class="action" href="/signup">sign up</a> instead?') %>
+                  <%- format(gettext('<strong %(emailId)>Email</strong> is not registered. Would you like to <a %(signUpLink)>sign up</a> instead?'),
+                             { emailId: 'id="unknown_email"', signUpLink: 'class="action" href="/signup"' }) %>
                 </li>
 
             </ul>
@@ -57,7 +58,8 @@
             <ul class="notifications">
                 <li class="notification" id="verify_primary">
                   <p>
-                    <%- gettext('To verify that you own <strong id="primary_email">address</strong>, you must sign in with your provider.  A new window will be opened.') %>
+                    <%- format(gettext('To verify that you own <strong %(primaryId)>address</strong>, you must sign in with your provider.  A new window will be opened.'),
+                               { primaryId: 'id="primary_email"' }) %>
                   </p>
 
                   <p>
diff --git a/resources/views/signup.ejs b/resources/views/signup.ejs
index 14e823d4f5c38012dd5a14151699bbefc597962f..364e1ad40a2453d138fd69fd20f53617a0757cbe 100644
--- a/resources/views/signup.ejs
+++ b/resources/views/signup.ejs
@@ -10,14 +10,15 @@
 
             <ul class="notifications">
                 <li class="notification alreadyRegistered">
-                  <%- gettext('<strong id="registeredEmail"></strong> is already registered. Would you like to <a class="action" href="/signin">sign in</a> instead?') %>
+                  <%- format(gettext('<strong %(emailId)></strong> is already registered. Would you like to <a %(signInLink)>sign in</a> instead?'),
+                             { emailId: 'id="registeredEmail"', signInLink: 'class="action" href="/signin"' }) %>
                 </li>
 
                 <li class="notification emailsent">
                   <h2><%- gettext('Confirm your email address') %></h2>
 
                   <p>
-                    <%- gettext('Check your email at <strong id="sentToEmail"></strong>.') %>
+                    <%- format(gettext('Check your email at <strong %(emailId)></strong>.'), { emailId: 'id="sentToEmail"' }) %>
                   </p>
 
                   <p>
@@ -94,7 +95,7 @@
                 <!-- This has to go down here because of the button.  Firefox clicks the first button in the form whenever the user hits enter in the form, whether the button is shown or not, and even if there is an input[type=submit].  Ghetto.  -->
                 <li class="notification" id="primary_verify">
                   <p>
-                    <%- gettext('To verify that you own <strong id="primary_email">address</strong>, you must sign in with your provider.  A new window will be opened.') %>
+                    <%- format(gettext('To verify that you own <strong %(emailId)>address</strong>, you must sign in with your provider.  A new window will be opened.'), { emailId: 'id="primary_email"' }) %>
                   </p>
 
                   <p class="submit">
diff --git a/resources/views/unsupported_dialog.ejs b/resources/views/unsupported_dialog.ejs
index 01340c242be8f592d8a1f76fa488d1e877d69eb9..942164297d1a294ccdaa5efc658660980300a9e9 100644
--- a/resources/views/unsupported_dialog.ejs
+++ b/resources/views/unsupported_dialog.ejs
@@ -13,11 +13,11 @@
       </a>
 
       <p>
-        <%- gettext('Persona works with <a href="http://getfirefox.com" target="_blank" title="Get Firefox">Firefox</a>') %>
+        <%- format(gettext('Persona works with <a %(getFirefoxLink)>Firefox</a>'), { getFirefoxLink: 'href="http://getfirefox.com" target="_blank" title="Get Firefox"' }) %>
       </p>
 
       <p class="lighter">
-       <%- gettext('and other <a href="http://whatbrowser.org" target="_blank">modern browsers.</a>') %>
+       <%- format(gettext('and other <a %(otherBrowserLink)>modern browsers.</a>'), { otherBrowserLink: 'href="http://whatbrowser.org" target="_blank"' }) %>
       </p>
 
   </section>
diff --git a/scripts/browserid.spec b/scripts/browserid.spec
index 879bf3490f42dcbaeb683374c9838c8786e03adc..eec44921a889561619453a24db88084a517d8a14 100644
--- a/scripts/browserid.spec
+++ b/scripts/browserid.spec
@@ -1,7 +1,7 @@
 %define _rootdir /opt/browserid
 
 Name:          browserid-server
-Version:       0.2012.07.20
+Version:       0.2012.08.03
 Release:       1%{?dist}_%{svnrev}
 Summary:       BrowserID server
 Packager:      Pete Fritchman <petef@mozilla.com>
diff --git a/tests/cache-header-tests.js b/tests/cache-header-tests.js
index da97f16712a38a3f8288bf5ce373fb13daccf17d..104ef5f5c6d9bf6d2bc6676cf67a465c87061e22 100755
--- a/tests/cache-header-tests.js
+++ b/tests/cache-header-tests.js
@@ -74,8 +74,6 @@ function hasProperCacheHeaders(path) {
       assert.strictEqual(r.statusCode, 200);
       // check X-Frame-Option headers
       hasProperFramingHeaders(r, path);
-      // ensure vary headers
-      assert.strictEqual(r.headers['vary'], 'Accept-Encoding,Accept-Language');
       // ensure public, max-age=0
       assert.strictEqual(r.headers['cache-control'], 'public, max-age=0');
       // the behavior of combining a last-modified date and an etag is undefined by
@@ -137,6 +135,20 @@ suite.addBatch({
 //  '/.well-known/browserid': hasProperCacheHeaders('/.well-known/browserid')
 });
 
+// related to cache headers are correct headers which let us serve static resources
+// (not rendered views) from a different domain, to support CDN compat
+suite.addBatch({
+  "static resources": {
+    topic: function() {
+      doRequest("/favicon.ico", {}, this.callback);
+    },
+    "have proper access control headers": function(err, r) {
+      assert.strictEqual(r.statusCode, 200);
+      assert.strictEqual(r.headers['access-control-allow-origin'],"http://127.0.0.1:10002");
+    }
+  }
+});
+
 // shut the server down and cleanup
 if (!process.env['SERVER_URL']) {
   start_stop.addShutdownBatches(suite);
diff --git a/tests/forgotten-pass-test.js b/tests/forgotten-pass-test.js
index 7e9aae487b38946332a3096323207edc3ad5e329..b13b5ddc14a69ff6f839bbd1e658fe86cc1abf1c 100755
--- a/tests/forgotten-pass-test.js
+++ b/tests/forgotten-pass-test.js
@@ -25,6 +25,9 @@ start_stop.addStartupBatches(suite);
 // var 'token'
 var token = undefined;
 
+// stores wsapi client context
+var oldContext;
+
 // create a new account via the api with (first address)
 suite.addBatch({
   "staging an account": {
@@ -101,6 +104,52 @@ suite.addBatch({
   }
 });
 
+// should not require auth to complete
+suite.addBatch({
+  "given a token, getting an email": {
+    topic: function() {
+      wsapi.get('/wsapi/email_for_token', { token: token }).call(this);
+    },
+    "account created": function(err, r) {
+      assert.equal(r.code, 200);
+      var body = JSON.parse(r.body);
+      assert.strictEqual(body.success, true);
+      assert.strictEqual(body.must_auth, false);
+    }
+  }
+});
+
+
+// New context for a second client
+suite.addBatch({
+  "change context": function () {
+    oldContext = wsapi.getContext();
+    wsapi.setContext({});
+  }
+});
+
+// should require auth to complete for second client
+suite.addBatch({
+  "given a token, getting an email": {
+    topic: function() {
+      wsapi.get('/wsapi/email_for_token', { token: token }).call(this);
+    },
+    "account created": function(err, r) {
+      assert.equal(r.code, 200);
+      var body = JSON.parse(r.body);
+      assert.strictEqual(body.success, true);
+      assert.strictEqual(body.must_auth, true);
+    }
+  }
+});
+
+// restore context of first client
+suite.addBatch({
+  "restore context": function () {
+    wsapi.setContext(oldContext);
+  }
+});
+
 // confirm second email email address to the account
 suite.addBatch({
   "create second account": {
@@ -183,6 +232,7 @@ suite.addBatch({
       assert.equal(r.code, 200);
       var body = JSON.parse(r.body);
       assert.strictEqual(body.success, true);
+      assert.strictEqual(body.must_auth, false);
     }
   }
 });
@@ -286,6 +336,67 @@ suite.addBatch({
   },
 });
 
+// Test issue #2104: when using a second browser to initiate password reset, first
+// browser should be prompted to authenticate
+
+// New context for a second client
+suite.addBatch({
+  "change context": function () {
+    oldContext = wsapi.getContext();
+    wsapi.setContext({});
+  }
+});
+
+// Run the "forgot_email" flow with first address. 
+suite.addBatch({
+  "reset password on first account": {
+    topic: wsapi.post('/wsapi/stage_reset', {
+      email: 'first@fakeemail.com',
+      pass: 'secondfakepass',
+      site:'https://otherfakesite.com'
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      start_stop.waitForToken(this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
+    }
+  }
+});
+
+// restore context of first client
+suite.addBatch({
+  "restore context": function () {
+    wsapi.setContext(oldContext);
+  }
+});
+
+suite.addBatch({
+  "given a token, getting an email": {
+    topic: function() {
+      wsapi.get('/wsapi/email_for_token', { token: token }).call(this);
+    },
+    "account created": function(err, r) {
+      assert.equal(r.code, 200);
+      var body = JSON.parse(r.body);
+      assert.strictEqual(body.success, true);
+      assert.strictEqual(body.email, 'first@fakeemail.com');
+      assert.strictEqual(body.must_auth, true);
+    }
+  }
+});
+
+
 // test list emails
 suite.addBatch({
   "list emails API": {
diff --git a/tests/lib/wsapi.js b/tests/lib/wsapi.js
index 868c86d02756fd53ed624cbe543b403355cb510e..cd35cb64be9a49c0865901ad410e1e9b434c9a32 100644
--- a/tests/lib/wsapi.js
+++ b/tests/lib/wsapi.js
@@ -42,4 +42,13 @@ exports.getCSRF = function() {
     return context.session.csrf_token;
   }
   return null;
-};
\ No newline at end of file
+};
+
+// allows for multiple clients
+exports.setContext = function (cxt) {
+  context = cxt;
+};
+
+exports.getContext = function () {
+  return context;
+};
diff --git a/tests/metrics-header-test.js b/tests/metrics-header-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..596e3a210d6069599202dd9627c8d43f5316ae0c
--- /dev/null
+++ b/tests/metrics-header-test.js
@@ -0,0 +1,100 @@
+#!/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'),
+fs = require('fs'),
+path = require('path'),
+http = require('http'),
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js'),
+urlparse = require('urlparse');
+
+var suite = vows.describe('metrics header test');
+suite.options.error = false;
+
+// allow this unit test to be targeted
+var SERVER_URL = process.env['SERVER_URL'] || 'http://127.0.0.1:10002/';
+
+process.env.METRICS_LOG_FILE = path.resolve(path.join(__dirname, 'data', 'metrics.json'));
+
+if (!process.env['SERVER_URL']) {
+  // start up a pristine server if we're locally testing
+  start_stop.addStartupBatches(suite);
+}
+
+// existsSync moved from path in 0.6.x to fs in 0.8.x
+if (typeof fs.existsSync === 'function') {
+  var existsSync = fs.existsSync;
+} else {
+  var existsSync = path.existsSync;
+}
+
+// now parse out host, port and scheme
+var purl = urlparse(SERVER_URL);
+const method = (purl.scheme === 'https') ? require('https') : require('http');
+
+function doRequest(path, headers, cb) {
+  var req = method.request({
+    port: purl.port,
+    host: purl.host,
+    path: path,
+    headers: headers,
+    agent: false
+  }, function(res) {
+    req.abort();
+    cb(null, res);
+  });
+  req.on('error', function(e) {
+    cb(e);
+  });
+  req.end();
+}
+
+suite.addBatch({
+  '/sign_in': {
+    topic: function() {
+      doRequest('/sign_in', {'user-agent': 'Test Runner', 'x-real-ip': '123.0.0.1', 'referer': 'https://persona.org'}, this.callback);
+    },
+    "metrics log exists": {
+      topic: function (err, r) {
+        if (existsSync(process.env.METRICS_LOG_FILE)) {
+          this.callback();
+        } else {
+          fs.watchFile(process.env.METRICS_LOG_FILE, null, this.callback);
+        }
+      },
+      "metric fields are logged properly": function (event, filename) {
+        var metrics = JSON.parse(fs.readFileSync(process.env.METRICS_LOG_FILE, "utf8").trim());
+        var message = JSON.parse(metrics.message);
+        assert.equal(message.ip, "123.0.0.1");
+        assert.equal(message.rp, "https://persona.org");
+        assert.equal(message.browser, "Test Runner");
+        fs.unwatchFile(process.env.METRICS_LOG_FILE);
+      }
+    }
+  }
+});
+
+
+suite.addBatch({
+  'clean up': function () {
+    fs.unlink(process.env.METRICS_LOG_FILE);
+    delete process.env.METRICS_LOG_FILE;
+  }
+});
+
+// shut the server down and cleanup
+if (!process.env['SERVER_URL']) {
+  start_stop.addShutdownBatches(suite);
+}
+
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);