diff --git a/lib/wsapi/update_password.js b/lib/wsapi/update_password.js
index b7698696e35f9f4e39cc2640c2a59f22b3b1e7b4..9cbad100ff263c6a7d80f23cf823d20b41f5390d 100644
--- a/lib/wsapi/update_password.js
+++ b/lib/wsapi/update_password.js
@@ -23,6 +23,11 @@ exports.process = function(req, res) {
         return res.json({ success: false });
       }
 
+      if (!success) {
+        logger.info("password update fails, incorrect old password");
+        return res.json({ success: false });
+      }
+
       logger.info("updating password for email " + req.session.authenticatedUser);
       wsapi.bcryptPassword(req.body.newpass, function(err, hash) {
         if (err) {
diff --git a/resources/static/css/style.css b/resources/static/css/style.css
index 37e2a3a01a27c09b324110e2671851b781b67bd8..8b23ff1b3a6ffce815152264157a5276596c481c 100644
--- a/resources/static/css/style.css
+++ b/resources/static/css/style.css
@@ -271,38 +271,36 @@ div.steps {
   font-weight: normal;
 }
 
-#manage .buttonrow {
-  margin: 72px 0 14px;
+#manage section {
+  margin-bottom: 20px;
+}
+
+.buttonrow {
+  margin: 0 0 14px;
 }
 
-#manage .buttonrow button {
+.buttonrow > h2 {
+  display: inline-block;
+  font-size: 1em;
+}
+
+#manage button {
   line-height: 20px;
   height: 24px;
-  width: 48px;
   font-size: 12px;
 }
 
-#manageAccounts {
-  background-color: #37A6FF;
-  border: 1px solid #37A6FF;
-  text-shadow: -1px -1px 0 #37A6FF;
-  cursor: pointer;
-
-  -webkit-box-shadow: 0 0 0 1px #76C2FF inset;
-     -moz-box-shadow: 0 0 0 1px #76C2FF inset;
-       -o-box-shadow: 0 0 0 1px #76C2FF inset;
-          box-shadow: 0 0 0 1px #76C2FF inset;
-
-  background-image: -moz-linear-gradient(#76C2FF 0pt, #37A6FF 100%);
-  background-image: -o-linear-gradient(#76C2FF 0pt, #37A6FF 100%);
-  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #76C2FF), color-stop(100%, #37A6FF));
+.buttonrow > button {
+  width: 48px;
 }
 
-.edit #manageAccounts {
+.buttonrow > .edit { }
+
+.edit .buttonrow > .edit {
   display: none;
 }
 
-#cancelManage {
+.buttonrow > .done {
   display: none;
   background-color: #006EC6;
   border: 1px solid #003E70;
@@ -319,13 +317,12 @@ div.steps {
   background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #3AA7FF), color-stop(100%, #006EC6));
 }
 
-.edit #cancelManage {
+.edit .buttonrow > .done {
   display: inline-block;
 }
 
 #manage #emailList {
   list-style-type: none;
-  margin: 0 0 72px 0;
   border-top: 1px solid #eee;
 }
 
@@ -376,10 +373,7 @@ div.steps {
 
 button.delete {
   background-color: #EA7676;
-  height: 24px;
-  vertical-align: middle;
   border: 1px solid #B13D3D;
-  font-size: 12px;
   font-weight: bold;
   font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
   color: #fff;
@@ -417,6 +411,32 @@ button.delete:active {
   background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #C84343), color-stop(100%, #AA3D3D));
 }
 
+#edit_password {
+  margin-bottom: 10px;
+}
+
+#edit_password label {
+  width: 40%;
+  display: inline-block;
+}
+
+#edit_password input[type=password] {
+  width: 40%;
+}
+
+.showedit {
+  opacity: 0;
+  -webkit-transition: all 500ms;
+  -moz-transition: all 500ms;
+  -ms-transition: all 500ms;
+  -o-transition: all 500ms;
+  transition: all 500ms;
+}
+
+.edit .showedit {
+  opacity: 1;
+}
+
 #disclaimer {
   color: #888;
   text-align: right;
@@ -683,7 +703,7 @@ a.forgot {
 }
 
 
-header {
+#wrapper > header {
   position: absolute;
   top: 0;
   font-weight: bold;
@@ -725,7 +745,7 @@ header a.signIn:hover, header a.signOut:hover {
   display: inline;
 }
 
-header,
+#wrapper > header,
 footer {
   display: block;
   width: 100%;
diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js
index 9e253e9ae89924c3918bbc39caa337acf1a9a83f..bd2725383de366afee6137f3be1b4b030c3ad932 100644
--- a/resources/static/dialog/controllers/actions.js
+++ b/resources/static/dialog/controllers/actions.js
@@ -1,4 +1,4 @@
-/*jshint brgwser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
 /*global _: true, BrowserID: true, PageController: true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
@@ -168,8 +168,15 @@ BrowserID.Modules.Actions = (function() {
           authenticated: authenticated
         });
       }, self.getErrorDialog(errors.checkAuthentication));
-    }
+    },
 
+    doVerifyPrimaryUser: function(info) {
+      startService("verify_primary_user", info);
+    },
+
+    doPrimaryUserVerified: function() {
+      // XXX we've gotta do something here too.
+    }
   });
 
   sc = Module.sc;
diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js
index 3149d5aab815d0a5ddc77a29ea7594bed203e44d..9d09e264f90511f68fe84469a8ec68efe9745585 100644
--- a/resources/static/dialog/controllers/authenticate.js
+++ b/resources/static/dialog/controllers/authenticate.js
@@ -128,7 +128,6 @@ BrowserID.Modules.Authenticate = (function() {
   }
 
   function createUserState() {
-
     var self=this;
 
     self.publish("create_user");
diff --git a/resources/static/dialog/controllers/verify_primary_user.js b/resources/static/dialog/controllers/verify_primary_user.js
new file mode 100644
index 0000000000000000000000000000000000000000..2725849454c3e8531eaf96869170d93157a42614
--- /dev/null
+++ b/resources/static/dialog/controllers/verify_primary_user.js
@@ -0,0 +1,60 @@
+/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*global _: true, BrowserID: true, PageController: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+BrowserID.Modules.VerifyPrimaryUser = (function() {
+  "use strict";
+
+  var bid = BrowserID,
+      sc;
+
+  var Module = bid.Modules.PageModule.extend({
+    start: function(data) {
+      var self=this;
+
+      data = data || {};
+
+      // XXX YEEHA!  We've gotta set some variables and redirect off to the
+      // IdPs page.
+      sc.start.call(this, data);
+    }
+
+  });
+
+  sc = Module.sc;
+
+  return Module;
+}());
+
diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js
index 5c5d516672d8ad92e56d7f176732b8877466448c..4449bc51858fa427a4545521683c5e46efcf56b5 100644
--- a/resources/static/dialog/resources/helpers.js
+++ b/resources/static/dialog/resources/helpers.js
@@ -91,17 +91,50 @@
   }
 
   function createUser(email, callback) {
+    function complete(status) {
+      callback && callback(status);
+    }
+
     var self=this;
-    user.createUser(email, function(staged) {
-      if (staged) {
-        self.close("user_staged", {
-          email: email
-        });
-      }
-      else {
-        tooltip.showTooltip("#could_not_add");
+    user.createUser(email, function(status) {
+      switch(status) {
+        case "secondary.already_added":
+          // XXX how to handle this - createUser should not be called on
+          // already existing addresses, so this path should not be called.
+          tooltip.showTooltip("#already_registered");
+          complete(false);
+          break;
+        case "secondary.verify":
+          self.close("user_staged", {
+            email: email
+          });
+          complete(true);
+          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":
+          self.close("primary_user_verified", {
+            email: email
+          });
+          complete(true);
+          break;
+        case "primary.verify":
+          self.close("primary_verify_user", {
+            email: email
+          });
+          complete(true);
+          break;
+        case "primary.could_not_add":
+          // XXX Can this happen?
+          break;
+        default:
+          break;
       }
-      if (callback) callback(staged);
     }, self.getErrorDialog(errors.createUser, callback));
   }
 
diff --git a/resources/static/dialog/resources/state_machine.js b/resources/static/dialog/resources/state_machine.js
index beb9f72d2cc2bad0a89ef92158b2544abc2c79c4..00f401da5f9927efbbaa3a4dc054b139fd4b9a6d 100644
--- a/resources/static/dialog/resources/state_machine.js
+++ b/resources/static/dialog/resources/state_machine.js
@@ -118,7 +118,6 @@
       var authenticated = info.authenticated;
 
       if (self.requiredEmail) {
-        // XXX get this out of here and into the state machine!
         gotoState("doAuthenticateWithRequiredEmail", {
           email: self.requiredEmail,
           authenticated: authenticated
@@ -147,6 +146,14 @@
       gotoState("doEmailConfirmed");
     });
 
+    subscribe("primary_user_verified", function(msg, info) {
+      gotoState("doPrimaryUserVerified", info);
+    });
+
+    subscribe("primary_verify_user", function(msg, info) {
+      gotoState("doVerifyPrimaryUser", info);
+    });
+
     subscribe("authenticate_with_required_email", function(msg, info) {
       gotoState("doAuthenticateWithRequiredEmail", info);
     });
diff --git a/resources/static/dialog/start.js b/resources/static/dialog/start.js
index a6787584ab511e2a3559278560f33ea12aae4733..944649ef074790d16f4c2c782cbfb2bd1720e670 100644
--- a/resources/static/dialog/start.js
+++ b/resources/static/dialog/start.js
@@ -18,6 +18,7 @@
         moduleManager.register("forgot_password", modules.ForgotPassword);
         moduleManager.register("pick_email", modules.PickEmail);
         moduleManager.register("required_email", modules.RequiredEmail);
+        moduleManager.register("verify_primary_user", modules.VerifyPrimaryUser);
 
         moduleManager.start("dialog");
       }
diff --git a/resources/static/dialog/views/authenticate.ejs b/resources/static/dialog/views/authenticate.ejs
index 8ac297c96a0d9ec2c157b7cb486537db3e1e03a1..376d2e6a1064890ac3a15bb8f3c0b8213dfa5959 100644
--- a/resources/static/dialog/views/authenticate.ejs
+++ b/resources/static/dialog/views/authenticate.ejs
@@ -17,6 +17,10 @@
               <div id="could_not_add" class="tooltip" for="email">
                 We just sent an email to that address!  If you really want to send another, wait a minute or two and try again.
               </div>
+
+              <div id="already_registered" class="tooltip" for="email">
+                That email address is already registered!  You should try again.
+              </div>
           </li>
 
           <li id="hint_section" class="start">
diff --git a/resources/static/pages/manage_account.js b/resources/static/pages/manage_account.js
index c606a69f42ade1ae55adbe5bfadcec9a25140243..0aa3a655f9ec8f5614dbf16a19b0d263cc30347d 100644
--- a/resources/static/pages/manage_account.js
+++ b/resources/static/pages/manage_account.js
@@ -45,7 +45,8 @@ BrowserID.manageAccount = (function() {
       pageHelpers = bid.PageHelpers,
       cancelEvent = pageHelpers.cancelEvent,
       confirmAction = confirm,
-      doc = document;
+      doc = document,
+      tooltip = bid.Tooltip;
 
   function relativeDate(date) {
     var diff = (((new Date()).getTime() - date.getTime()) / 1000),
@@ -211,12 +212,46 @@ BrowserID.manageAccount = (function() {
     }
   }
 
-  function manageAccounts() {
-      $("body").addClass("edit");
+  function startEdit(event) {
+    // XXX add some helpers in the dom library to find section.
+    event.preventDefault();
+    $(event.target).closest("section").addClass("edit");
   }
 
-  function cancelManage() {
-      $("body").removeClass("edit");
+  function cancelEdit(event) {
+    event.preventDefault();
+    $(event.target).closest("section").removeClass("edit");
+  }
+
+  function changePassword(oncomplete) {
+    var oldPassword = dom.getInner("#old_password"),
+        newPassword = dom.getInner("#new_password");
+
+    function complete(status) {
+      typeof oncomplete == "function" && oncomplete(status);
+    }
+
+    if(!oldPassword) {
+      tooltip.showTooltip("#tooltipOldRequired");
+      complete(false);
+    }
+    else if(!newPassword) {
+      tooltip.showTooltip("#tooltipNewRequired");
+      complete(false);
+    }
+    else {
+      user.changePassword(oldPassword, newPassword, function(status) {
+        if(status) {
+          dom.removeClass("#edit_password", "edit");
+        }
+        else {
+          tooltip.showTooltip("#tooltipInvalidPassword");
+        }
+
+        complete(status);
+      }, pageHelpers.getFailure(errors.updatePassword, oncomplete));
+    }
+
   }
 
   function displayHelpTextToNewUser() {
@@ -233,8 +268,10 @@ BrowserID.manageAccount = (function() {
     if (options.confirm) confirmAction = options.confirm;
 
     dom.bindEvent("#cancelAccount", "click", cancelEvent(cancelAccount));
-    dom.bindEvent("#manageAccounts", "click", cancelEvent(manageAccounts));
-    dom.bindEvent("#cancelManage", "click", cancelEvent(cancelManage));
+
+    dom.bindEvent("button.edit", "click", startEdit);
+    dom.bindEvent("button.done", "click", cancelEdit);
+    dom.bindEvent("#edit_password_form", "submit", cancelEvent(changePassword));
 
     syncAndDisplayEmails(oncomplete);
 
@@ -244,13 +281,16 @@ BrowserID.manageAccount = (function() {
   // BEGIN TESTING API
   function reset() {
     dom.unbindEvent("#cancelAccount", "click");
-    dom.unbindEvent("#manageAccounts", "click");
-    dom.unbindEvent("#cancelManage", "click");
+
+    dom.unbindEvent("button.edit", "click");
+    dom.unbindEvent("button.done", "click");
+    dom.unbindEvent("#edit_password_form", "submit");
   }
 
   init.reset = reset;
   init.cancelAccount = cancelAccount;
   init.removeEmail = removeEmail;
+  init.changePassword = changePassword;
   // END TESTING API
 
   return init;
diff --git a/resources/static/pages/page_helpers.js b/resources/static/pages/page_helpers.js
index c18b2817c54e3c336841f8bde097c31995ea95e2..e024ee1750279d617dedb8d3165a68403cb35f59 100644
--- a/resources/static/pages/page_helpers.js
+++ b/resources/static/pages/page_helpers.js
@@ -94,7 +94,7 @@ BrowserID.PageHelpers = (function() {
       $("#errorBackground").stop().fadeIn();
       $("#error").stop().fadeIn();
 
-      callback && callback();
+      callback && callback(false);
     }
   }
 
diff --git a/resources/static/pages/signup.js b/resources/static/pages/signup.js
index fda1de0e63a143bb7c367b017764099729a359b9..a70e72fc2b558cf97f595e7b8fb060e43e0e5a51 100644
--- a/resources/static/pages/signup.js
+++ b/resources/static/pages/signup.js
@@ -55,45 +55,45 @@ BrowserID.signUp = (function() {
     function submit(oncomplete) {
       var email = helpers.getAndValidateEmail("#email");
 
-      function complete() {
-        oncomplete && oncomplete();
+      function complete(status) {
+        oncomplete && oncomplete(status);
       }
 
       if (email) {
-        user.addressInfo(email, function(info) {
-          if (info.type === 'secondary') {
-            if (!info.known) {
-              user.createUser(email, function onSuccess(success) {
-                if(success) {
-                  pageHelpers.showEmailSent(oncomplete);
-                }
-                else {
-                  tooltip.showTooltip("#could_not_add");
-                  complete();
-                }
-              }, pageHelpers.getFailure(errors.createUser, oncomplete));
-            }
-            else {
+        user.createUser(email, function onComplete(status) {
+          switch(status) {
+            case "secondary.already_added":
               $('#registeredEmail').html(email);
               showNotice(".alreadyRegistered");
-              complete();
-            }
-          } else {
-            BrowserID.Provisioning({
-              email: email,
-              url: info.prov
-            }, function(r) {
-              // XXX: implement me
-              alert("shane!  provisioning was a success " + JSON.stringify(r));
-            }, function(e) {
-              // XXX: implement me
-              alert("shane!  provisioning was a failure: " + JSON.stringify(e));
-            });
+              complete(false);
+              break;
+            case "secondary.verify":
+              pageHelpers.showEmailSent(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.replaceInputsWithNotice("#congrats", complete.bind(null, true));
+              break;
+            case "primary.verify":
+              // XXX What do we do here?
+              complete(false);
+              break;
+            case "primary.could_not_add":
+              // XXX Can this happen?
+              break;
+            default:
+              break;
           }
-        }, pageHelpers.getFailure(errors.isEmailRegistered, oncomplete));
+        }, pageHelpers.getFailure(errors.createUser, oncomplete));
       }
       else {
-        complete();
+        complete(false);
       }
     }
 
@@ -105,7 +105,9 @@ BrowserID.signUp = (function() {
       if (event.which !== 13) $(".notification").fadeOut(ANIMATION_SPEED);
     }
 
-    function init() {
+    function init(config) {
+      config = config || {};
+
       $("form input[autofocus]").focus();
 
       pageHelpers.setupEmail();
diff --git a/resources/static/shared/error-messages.js b/resources/static/shared/error-messages.js
index f1e6cbb681922c7fce6ce5f9978b0a3094b34fa8..dc444e193b35b29526501d9ffefaae83ed91ad4d 100644
--- a/resources/static/shared/error-messages.js
+++ b/resources/static/shared/error-messages.js
@@ -82,6 +82,11 @@ BrowserID.Errors = (function(){
       message: "Unfortunately, BrowserID cannot communicate while offline!"
     },
 
+    provisioningPrimary: {
+      title: "Provisioning Primary",
+      message: "We had trouble communicating with your email provider, please try again!"
+    },
+
     registration: {
       title: "Registration Failed"
     },
@@ -119,6 +124,10 @@ BrowserID.Errors = (function(){
       title: "Sync Keys for Address"
     },
 
+    updatePassword: {
+      title: "Updating password"
+    },
+
     xhrError: {
       title: "Communication Error"
     }
diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js
index ab3b6e96b77eb9ab3b2b8e13a875768a071b2444..16c0f60b82cedfe6118b6b9370f1129fb0ac9e84 100644
--- a/resources/static/shared/network.js
+++ b/resources/static/shared/network.js
@@ -120,7 +120,7 @@ BrowserID.Network = (function() {
     else {
       var url = "/wsapi/session_context";
       xhr.ajax({
-        url: "/wsapi/session_context",
+        url: url,
         success: function(result) {
           csrf_token = result.csrf_token;
           server_time = {
@@ -379,15 +379,22 @@ BrowserID.Network = (function() {
      * @method changePassword
      * @param {string} oldpassword - old password.
      * @param {string} newpassword - new password.
-     * @param {function} [onSuccess] - Callback to call when complete. Will be
+     * @param {function} [onComplete] - Callback to call when complete. Will be
      * called with true if successful, false otw.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    changePassword: function(oldPassword, newPassword, onSuccess, onFailure) {
-      // XXX fill this in
-      if (onSuccess) {
-        onSuccess(true);
-      }
+    changePassword: function(oldPassword, newPassword, onComplete, onFailure) {
+      post({
+        url: "/wsapi/update_password",
+        data: {
+          oldpass: oldPassword,
+          newpass: newPassword
+        },
+        success: function(response) {
+          if (onComplete) onComplete(response.success);
+        },
+        error: onFailure
+      });
     },
 
 
@@ -420,8 +427,8 @@ BrowserID.Network = (function() {
           email: email,
           site: origin
         },
-        success: function(status) {
-          if (onSuccess) onSuccess(status.success);
+        success: function(response) {
+          if (onSuccess) onSuccess(response.success);
         },
         error: function(info) {
           // 403 is throttling.
@@ -474,7 +481,7 @@ BrowserID.Network = (function() {
      * (is it a primary or a secondary)
      * @method addressInfo
      * @param {string} email - Email address to check.
-     * @param {function} [onSuccess] - Called with an object on success,
+     * @param {function} [onComplete] - Called with an object on success,
      *   containing these properties:
      *     type: <secondary|primary>
      *     known: boolean, present - present if type is secondary
@@ -482,11 +489,11 @@ BrowserID.Network = (function() {
      *     prov: string - url to embed for silent provisioning - present if type is secondary
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    addressInfo: function(email, onSuccess, onFailure) {
+    addressInfo: function(email, onComplete, onFailure) {
       get({
         url: "/wsapi/address_info?email=" + encodeURIComponent(email),
         success: function(data, textStatus, xhr) {
-          if (onSuccess) onSuccess(data);
+          if (onComplete) onComplete(data);
         },
         error: onFailure
       });
diff --git a/resources/static/shared/tooltip.js b/resources/static/shared/tooltip.js
index eb512c0f90e5dbcf75d4a7f9412378640c4ee854..14d21932fee721cc290077c1f3eff982e2b52376 100644
--- a/resources/static/shared/tooltip.js
+++ b/resources/static/shared/tooltip.js
@@ -44,7 +44,7 @@ BrowserID.Tooltip = (function() {
       bid = BrowserID,
       dom = bid.DOM,
       renderer = bid.Renderer,
-      lastTooltip;
+      hideTimer;
 
   function createTooltip(el) {
       var contents = el.html();
@@ -74,7 +74,7 @@ BrowserID.Tooltip = (function() {
 
     bid.Tooltip.shown = true;
     el.fadeIn(ANIMATION_TIME, function() {
-      setTimeout(function() {
+      hideTimer = setTimeout(function() {
         el.fadeOut(ANIMATION_TIME, function() {
           bid.Tooltip.shown = false;
           if(complete) complete();
@@ -118,7 +118,19 @@ BrowserID.Tooltip = (function() {
 
 
  return {
-    showTooltip: showTooltip
+   showTooltip: showTooltip
+   // BEGIN TESTING API
+   ,
+   reset: function() {
+     if(hideTimer) {
+       clearTimeout(hideTimer);
+       hideTimer = null;
+     }
+
+     $(".tooltip").hide();
+     bid.Tooltip.shown = false;
+   }
+   // END TESTING API
  };
 
 }());
diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js
index a9e85b463d6b8d5c46343b536cb6c63ce1afb903..58509bb8c22d20933c9ec1623a95e182f9ba0441 100644
--- a/resources/static/shared/user.js
+++ b/resources/static/shared/user.js
@@ -39,9 +39,11 @@ BrowserID.User = (function() {
   "use strict";
 
   var jwk, jwt, vep, jwcert, origin,
-      network = BrowserID.Network,
-      storage = BrowserID.Storage,
-      User, pollTimeout;
+      bid = BrowserID,
+      network = bid.Network,
+      storage = bid.Storage,
+      User, pollTimeout,
+      provisioning = bid.Provisioning;
 
   function prepareDeps() {
     if (!jwk) {
@@ -52,7 +54,6 @@ BrowserID.User = (function() {
     }
   }
 
-  "use strict";
   // remove identities that are no longer valid
   function cleanupIdentities(cb) {
     network.serverTime(function(serverTime) {
@@ -220,6 +221,16 @@ BrowserID.User = (function() {
   }
 
   User = {
+    init: function(config) {
+      if(config.provisioning) {
+        provisioning = config.provisioning;
+      }
+    },
+
+    reset: function() {
+      provisioning = BrowserID.Provisioning;
+    },
+
     /**
      * Set the interface to use for networking.  Used for unit testing.
      * @method setNetwork
@@ -259,18 +270,70 @@ BrowserID.User = (function() {
 
     /**
      * Create a user account - this creates an user account that must be verified.
-     * @method createUser
+     * @method createSecondaryUser
      * @param {string} email - Email address.
-     * @param {function} [onSuccess] - Called on successful completion.
+     * @param {function} [onComplete] - Called on completion.
      * @param {function} [onFailure] - Called on error.
      */
-    createUser: function(email, onSuccess, onFailure) {
+    createSecondaryUser: function(email, onComplete, onFailure) {
       var self=this;
 
       // remember this for later
       storage.setStagedOnBehalfOf(self.getHostname());
 
-      network.createUser(email, origin, onSuccess, onFailure);
+      network.createUser(email, origin, onComplete, onFailure);
+    },
+
+    /**
+     * Status:
+     * "already_added", "verify_secondary", "secondary_could_not_add", "verify_primary",
+     * "primary_verified"
+     */
+    createUser: function(email, onComplete, onFailure) {
+      var self=this;
+
+      function attemptAddSecondary(email, info) {
+        if (info.known) {
+          onComplete("secondary.already_added");
+        }
+        else {
+          self.createSecondaryUser(email, function(success) {
+            if(success) {
+              onComplete("secondary.verify");
+            }
+            else {
+              onComplete("secondary.could_not_add");
+            }
+          }, onFailure);
+        }
+      }
+
+      function attemptAddPrimary(email, info) {
+        // XXX Can we know if the primary is already known to us?
+        provisioning({
+          email: email,
+          url: info.prov
+        }, function(r) {
+          onComplete("primary.verified");
+        }, function(info) {
+          // XXX When do we have to redirect off to the authentication page?
+          // Would a code like this come in on the failure mode?
+          if(info.code === "MUST_AUTHENTICATE") {
+            onComplete("primary.verify");
+          }
+          else {
+            onFailure(info);
+          }
+        });
+      }
+
+      network.addressInfo(email, function(info) {
+        if (info.type === 'secondary') {
+          attemptAddSecondary(email, info);
+        } else {
+          attemptAddPrimary(email, info);
+        }
+      }, onFailure);
     },
 
     /**
@@ -322,7 +385,7 @@ BrowserID.User = (function() {
     },
 
     /**
-     * Set the password of the current user.
+     * Set the initial password of the current user.
      * @method setPassword
      * @param {string} password - password to set
      * @param {function} [onComplete] - Called on successful completion.
@@ -332,6 +395,20 @@ BrowserID.User = (function() {
       network.setPassword(password, onComplete, onFailure);
     },
 
+    /**
+     * update the password of the current user.
+     * @method changePassword
+     * @param {string} oldpassword - the old password.
+     * @param {string} newpassword - the new password.
+     * @param {function} [onComplete] - called on completion.  Called with one
+     * parameter, status - set to true if password update is successful, false
+     * otw.
+     * @param {function} [onFailure] - called on XHR failure.
+     */
+    changePassword: function(oldpassword, newpassword, onComplete, onFailure) {
+      network.changePassword(oldpassword, newpassword, onComplete, onFailure);
+    },
+
     /**
      * Request a password reset for the given email address.
      * @method requestPasswordReset
diff --git a/resources/static/test/index.html b/resources/static/test/index.html
index a417d34a14e472782afe21a94151bbf3bbe0ef9d..50386e33eb941857f92ac59b623fd44155900a58 100644
--- a/resources/static/test/index.html
+++ b/resources/static/test/index.html
@@ -40,12 +40,10 @@
         <input id="newEmail" />
         <input id="password" />
         <input id="vpassword" />
+        <input id="old_password" />
+        <input id="new_password" />
         <input type="checkbox" id="remember" />
       </div>
-      <div id="congrats">Congrats!</div>
-      <span id="cannotconfirm" class="error">Cannot confirm</span>
-      <span id="cannotcommunicate" class="error">Cannot communicate</span>
-      <span class="siteinfo" class="error"><span class="website"></span></span>
       <span class="hint">Hint</span>
     </div>
 
@@ -61,8 +59,12 @@
 
 
     <ul class="notifications">
+      <li id="cannotconfirm" class="error notification">Cannot confirm</li>
+      <li id="cannotcommunicate" class="error notification">Cannot communicate</li>
+      <li class="siteinfo" class="error notification"><span class="website"></span></li>
       <li class="notification emailsent">Email Sent</li>
       <li class="notification doh">doh</li>
+      <li class="notification" id="congrats">Congratulations!</li>
     </ul>
 
     <ul id="emailList">
@@ -87,6 +89,7 @@
     <script type="text/javascript" src="qunit/mocks/mocks.js"></script>
     <script type="text/javascript" src="qunit/mocks/xhr.js"></script>
     <script type="text/javascript" src="qunit/mocks/templates.js"></script>
+    <script type="text/javascript" src="qunit/mocks/provisioning.js"></script>
     <script type="text/javascript" src="/shared/javascript-extensions.js"></script>
     <script type="text/javascript" src="/shared/renderer.js"></script>
     <script type="text/javascript" src="/shared/class.js"></script>
@@ -118,6 +121,7 @@
     <script type="text/javascript" src="/dialog/controllers/authenticate.js"></script>
     <script type="text/javascript" src="/dialog/controllers/forgotpassword.js"></script>
     <script type="text/javascript" src="/dialog/controllers/required_email.js"></script>
+    <script type="text/javascript" src="/dialog/controllers/verify_primary_user.js"></script>
 
     <script type="text/javascript" src="/pages/page_helpers.js"></script>
     <script type="text/javascript" src="/pages/add_email_address.js"></script>
@@ -164,7 +168,9 @@
     <script type="text/javascript" src="qunit/controllers/authenticate_unit_test.js"></script>
     <script type="text/javascript" src="qunit/controllers/forgotpassword_unit_test.js"></script>
     <script type="text/javascript" src="qunit/controllers/required_email_unit_test.js"></script>
-    // must go last or all other tests will fail.
+    <script type="text/javascript" src="qunit/controllers/verify_primary_user_unit_test.js"></script>
+
+    <!-- must go last or all other tests will fail. -->
     <script type="text/javascript" src="qunit/controllers/dialog_unit_test.js"></script>
 	</body>
 </html>
diff --git a/resources/static/test/qunit/controllers/actions_unit_test.js b/resources/static/test/qunit/controllers/actions_unit_test.js
index 340c91498eeb49cf8871136a0536c6407e1592b1..5e6aee990f6d9adde622e30659599062b4f4868c 100644
--- a/resources/static/test/qunit/controllers/actions_unit_test.js
+++ b/resources/static/test/qunit/controllers/actions_unit_test.js
@@ -95,5 +95,27 @@
     });
   });
 
+  /*
+  asyncTest("doVerifyPrimaryUser does something", function() {
+    createController({
+      ready: function() {
+        controller.doVerifyPrimaryUser();
+        // XXX test something
+        start();
+      }
+    });
+  });
+
+  asyncTest("doPrimaryUserVerified does something", function() {
+    createController({
+      ready: function() {
+        controller.doPrimaryUserVerified();
+        // XXX test something
+        start();
+      }
+    });
+  });
+*/
+
 }());
 
diff --git a/resources/static/test/qunit/controllers/authenticate_unit_test.js b/resources/static/test/qunit/controllers/authenticate_unit_test.js
index 7a0d73ed193d26e88687d874f29e307b5a7326c0..60763b5fa1c6588058e6ce144b109a6e7847e223 100644
--- a/resources/static/test/qunit/controllers/authenticate_unit_test.js
+++ b/resources/static/test/qunit/controllers/authenticate_unit_test.js
@@ -47,7 +47,9 @@
       userCreated = true,
       mediator = bid.Mediator,
       registrations = [],
-      register = bid.TestHelpers.register;
+      testHelpers = bid.TestHelpers,
+      register = testHelpers.register,
+      provisioning = bid.Mocks.Provisioning;
 
   function reset() {
     emailRegistered = false;
@@ -63,7 +65,7 @@
   module("controllers/authenticate", {
     setup: function() {
       reset();
-      bid.TestHelpers.setup();
+      testHelpers.setup();
       createController();
     },
 
@@ -76,7 +78,7 @@
         }
       }
       reset();
-      bid.TestHelpers.teardown();
+      testHelpers.teardown();
     }
   });
 
@@ -153,6 +155,9 @@
 
   asyncTest("createUser with valid email", function() {
     $("#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");
       start();
@@ -189,8 +194,6 @@
       equal(bid.Tooltip.shown, true, "tooltip is shown");
       start();
     });
-
-
   });
 
   asyncTest("createUser with valid email, XHR error", function() {
diff --git a/resources/static/test/qunit/controllers/verify_primary_user_unit_test.js b/resources/static/test/qunit/controllers/verify_primary_user_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..1a9390b0530be4443392dd51ff12f26b0a03a468
--- /dev/null
+++ b/resources/static/test/qunit/controllers/verify_primary_user_unit_test.js
@@ -0,0 +1,65 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+(function() {
+  "use strict";
+
+  var bid = BrowserID,
+      controller,
+      el,
+      testHelpers = bid.TestHelpers;
+
+  function createController(config) {
+    controller = BrowserID.Modules.VerifyPrimaryUser.create();
+    controller.start(config);
+  }
+
+  module("controllers/verify_primary_user", {
+    setup: function() {
+      testHelpers.setup();
+    },
+
+    teardown: function() {
+      if(controller) {
+        controller.destroy();
+      }
+      testHelpers.teardown();
+    }
+  });
+
+  // XXX Do some tests!
+}());
+
diff --git a/resources/static/test/qunit/mocks/mocks.js b/resources/static/test/qunit/mocks/mocks.js
index 70a00e1ffc8e09b63fc58f6d70ea3fe1d21e82b4..68d2223b336ebca0c85e02153e5ee84e2f637622 100644
--- a/resources/static/test/qunit/mocks/mocks.js
+++ b/resources/static/test/qunit/mocks/mocks.js
@@ -1,4 +1,3 @@
-
 /*jshint browsers:true, forin: true, laxbreak: true */
 /*global BrowserID: true */
 /* ***** BEGIN LICENSE BLOCK *****
diff --git a/resources/static/test/qunit/mocks/provisioning.js b/resources/static/test/qunit/mocks/provisioning.js
new file mode 100644
index 0000000000000000000000000000000000000000..70a8760e627c3de8546e748ee46cde2b4accd39f
--- /dev/null
+++ b/resources/static/test/qunit/mocks/provisioning.js
@@ -0,0 +1,54 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+BrowserID.Mocks.Provisioning = (function() {
+  function Provisioning(info, onsuccess, onfailure) {
+    if(Provisioning.failure) onfailure(Provisioning.failure);
+    else onsuccess();
+  }
+
+  Provisioning.setSuccess = function(status) {
+    Provisioning.status = status;
+  };
+
+  Provisioning.setFailure = function(status) {
+    Provisioning.failure = status;
+  }
+
+  return Provisioning;
+}());
+
+
diff --git a/resources/static/test/qunit/mocks/xhr.js b/resources/static/test/qunit/mocks/xhr.js
index a65e6e5bfd0a89c83b719b86e4dc151e70421679..e2f48b980ff484c45934263f4f03b531e89cbc7f 100644
--- a/resources/static/test/qunit/mocks/xhr.js
+++ b/resources/static/test/qunit/mocks/xhr.js
@@ -56,14 +56,8 @@ BrowserID.Mocks.xhr = (function() {
   var xhr = {
     results: {
       "get /wsapi/session_context valid": contextInfo,
-      "get /wsapi/session_context invalid": contextInfo,
       // We are going to test for XHR failures for session_context using
-      // call to serverTime.  We are going to use the flag contextAjaxError
-      "get /wsapi/session_context ajaxError": contextInfo,
-      "get /wsapi/session_context complete": contextInfo,
-      "get /wsapi/session_context throttle": contextInfo,
-      "get /wsapi/session_context multiple": contextInfo,
-      "get /wsapi/session_context no_identities": contextInfo,
+      // 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 invalid": { success: false },
@@ -76,6 +70,7 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/complete_email_addition valid": { success: true },
       "post /wsapi/complete_email_addition invalid": { success: false },
       "post /wsapi/complete_email_addition ajaxError": undefined,
+      "post /wsapi/stage_user unknown_secondary": { success: true },
       "post /wsapi/stage_user valid": { success: true },
       "post /wsapi/stage_user invalid": { success: false },
       "post /wsapi/stage_user throttle": 403,
@@ -115,7 +110,19 @@ BrowserID.Mocks.xhr = (function() {
       "get /wsapi/list_emails no_identities": [],
       "get /wsapi/list_emails ajaxError": undefined,
       // Used in conjunction with registration to do a complete userflow
-      "get /wsapi/list_emails complete": {"registered@testuser.com":{}}
+      "get /wsapi/list_emails complete": {"registered@testuser.com":{}},
+      "post /wsapi/update_password valid": { success: true },
+      "post /wsapi/update_password incorrectPassword": { success: false },
+      "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 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=unregistered%40testuser.com primary": { type: "primary", auth: "", prov: "" },
+      "get /wsapi/address_info?email=testuser%40testuser.com unknown_secondary": { type: "secondary", known: false },
+      "get /wsapi/address_info?email=testuser%40testuser.com known_secondary": { type: "secondary", known: true },
+      "get /wsapi/address_info?email=testuser%40testuser.com primary": { type: "primary", auth: "", prov: "" },
+      "get /wsapi/address_info?email=testuser%40testuser.com ajaxError": undefined
     },
 
     setContextInfo: function(field, value) {
@@ -145,7 +152,18 @@ BrowserID.Mocks.xhr = (function() {
         ok(false, "missing csrf token on POST request");
       }
 
-      var resName = req.type + " " + req.url + " " + xhr.resultType;
+
+      var resultType = xhr.resultType;
+
+      // Unless the contextAjaxError is specified, use the "valid" context info.
+      // This makes it so we do not have to keep adding new items for
+      // context_info for every possible result type.
+      if(req.url === "/wsapi/session_context" && resultType !== "contextAjaxError") {
+        resultType = "valid";
+      }
+
+      var resName = req.type + " " + req.url + " " + resultType;
+
       var result = xhr.results[resName];
 
       var type = typeof result;
diff --git a/resources/static/test/qunit/pages/manage_account_unit_test.js b/resources/static/test/qunit/pages/manage_account_unit_test.js
index 1906a5919202ad69d3eefc8934f9b5e8dfa1edd5..0c18b7e66161c0e440133638c21566a354908d13 100644
--- a/resources/static/test/qunit/pages/manage_account_unit_test.js
+++ b/resources/static/test/qunit/pages/manage_account_unit_test.js
@@ -44,6 +44,7 @@
       testHelpers = bid.TestHelpers,
       validToken = true,
       TEST_ORIGIN = "http://browserid.org",
+      tooltip = bid.Tooltip,
       mocks = {
         confirm: function() { return true; },
         document: { location: "" }
@@ -165,4 +166,73 @@
       });
     });
   });
+
+  asyncTest("changePassword with missing old password, expect 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");
+        equal(tooltip.shown, true, "tooltip is visible");
+        start();
+      });
+    });
+  });
+
+  asyncTest("changePassword with missing new password, expect 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");
+        equal(tooltip.shown, true, "tooltip is visible");
+        start();
+      });
+    });
+  });
+
+  asyncTest("changePassword with incorrect old password, expect 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");
+        equal(tooltip.shown, true, "tooltip is visible");
+        start();
+      });
+    });
+  });
+
+  asyncTest("changePassword with XHR error, expect error message", function() {
+    bid.manageAccount(mocks, function() {
+      xhr.useResult("invalid");
+
+      $("#old_password").val("oldpassword");
+      $("#new_password").val("newpassword");
+
+      bid.manageAccount.changePassword(function(status) {
+        equal(status, false, "on xhr error, status is false");
+        start();
+      });
+    });
+  });
+
+  asyncTest("changePassword 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");
+        start();
+      });
+    });
+  });
+
 }());
diff --git a/resources/static/test/qunit/pages/signup_unit_test.js b/resources/static/test/qunit/pages/signup_unit_test.js
index 24bf65256377ceb5371a54bcbe655530f3682324..b55022f0ad9df19b5fab4bbe3041618b34997a46 100644
--- a/resources/static/test/qunit/pages/signup_unit_test.js
+++ b/resources/static/test/qunit/pages/signup_unit_test.js
@@ -39,39 +39,35 @@
 
   var bid = BrowserID,
       network = bid.Network,
-      user = bid.User,
       xhr = bid.Mocks.xhr,
-      testOrigin = "http://browserid.org";
+      testOrigin = "http://browserid.org",
+      testHelpers = bid.TestHelpers,
+      provisioning = bid.Mocks.Provisioning;
 
   module("pages/signup", {
     setup: function() {
-      network.setXHR(xhr);
-      $(".error").removeClass("error");
-      $("#error").stop().hide();
-      $(".notification").stop().hide();
-      xhr.useResult("valid");
-      user.setOrigin(testOrigin);
+      testHelpers.setup();
       bid.signUp();
     },
     teardown: function() {
-      network.setXHR($);
-      $(".error").removeClass("error");
-      $("#error").stop().hide();
-      $(".notification").stop().hide();
-      $("#error .message").remove();
+      testHelpers.teardown();
       bid.signUp.reset();
     }
   });
 
-  function testNoticeNotVisible(extraTests) {
-    bid.signUp.submit(function() {
+  function testNotRegistered(extraTests) {
+    bid.signUp.submit(function(status) {
+      strictEqual(status, false, "address was not registered");
       equal($(".emailsent").is(":visible"), false, "email not sent, notice not visible");
+
       if(extraTests) extraTests();
       start();
     });
   }
 
-  asyncTest("signup with valid unregistered email", function() {
+  asyncTest("signup with valid unregistered secondary email", function() {
+    xhr.useResult("unknown_secondary");
+
     $("#email").val("unregistered@testuser.com");
 
     bid.signUp.submit(function() {
@@ -81,6 +77,8 @@
   });
 
   asyncTest("signup with valid unregistered email with leading/trailing whitespace", function() {
+    xhr.useResult("unknown_secondary");
+
     $("#email").val(" unregistered@testuser.com ");
 
     bid.signUp.submit(function() {
@@ -90,35 +88,37 @@
   });
 
   asyncTest("signup with valid registered email", function() {
+    xhr.useResult("known_secondary");
     $("#email").val("registered@testuser.com");
 
-    testNoticeNotVisible();
+    testNotRegistered();
   });
 
   asyncTest("signup with invalid email address", function() {
     $("#email").val("invalid");
 
-    testNoticeNotVisible();
+    testNotRegistered();
   });
 
   asyncTest("signup with throttling", function() {
     xhr.useResult("throttle");
 
-    $("#email").val("throttled@testuser.com");
+    $("#email").val("unregistered@testuser.com");
 
-    testNoticeNotVisible();
+    testNotRegistered();
   });
 
-  asyncTest("signup with invalid XHR error", function() {
+  asyncTest("signup with XHR error", function() {
     xhr.useResult("invalid");
     $("#email").val("unregistered@testuser.com");
 
-    testNoticeNotVisible(function() {
-      equal($("#error").is(":visible"), true, "error message displayed");
+    testNotRegistered(function() {
+      testHelpers.testErrorVisible();
     });
   });
 
-  asyncTest("signup with unregistered email and cancel button pressed", function() {
+  asyncTest("signup with unregistered secondary email and cancel button pressed", function() {
+    xhr.useResult("unknown_secondary");
     $("#email").val("unregistered@testuser.com");
 
     bid.signUp.submit(function() {
@@ -131,4 +131,51 @@
     });
   });
 
+  asyncTest("signup with primary email address, provisioning failure - expect error screen", function() {
+    xhr.useResult("primary");
+
+    $("#email").val("unregistered@testuser.com");
+    provisioning.setFailure({
+      code: "internal",
+      msg: "doowap"
+    });
+
+    bid.signUp.submit(function(status) {
+      equal(status, false, "provisioning failure, status false");
+      testHelpers.testErrorVisible();
+      start();
+    });
+  });
+
+  asyncTest("signup with primary email address, user verified by primary - print success message", function() {
+    xhr.useResult("primary");
+
+    $("#email").val("unregistered@testuser.com");
+
+    provisioning.setSuccess(true);
+
+    bid.signUp.submit(function(status) {
+      equal(status, true, "primary addition success - true status");
+      equal($(".notification:visible").length, 1, "success notification is visible");
+      start();
+    });
+  });
+
+  // XXX what do we expect here?
+  asyncTest("signup with primary email address, user must verify with primary - ", function() {
+    xhr.useResult("primary");
+
+    $("#email").val("unregistered@testuser.com");
+
+    provisioning.setFailure({
+      code: "MUST_AUTHENTICATE",
+      msg: "Wahhooo!!"
+    });
+
+    bid.signUp.submit(function(status) {
+      equal(status, false, "user must authenticate, some action needed.");
+      start();
+    });
+  });
+
 }());
diff --git a/resources/static/test/qunit/resources/helpers_unit_test.js b/resources/static/test/qunit/resources/helpers_unit_test.js
index ed31c29bae27dad4153274d65b2c8cc189d80259..66613847707e62012465aa9686671ff3980a5edc 100644
--- a/resources/static/test/qunit/resources/helpers_unit_test.js
+++ b/resources/static/test/qunit/resources/helpers_unit_test.js
@@ -44,8 +44,12 @@
       storage = bid.Storage,
       tooltip = bid.Tooltip,
       testHelpers = bid.TestHelpers,
+      user = bid.User,
+      provisioning = bid.Mocks.Provisioning,
       closeCB,
-      errorCB;
+      errorCB,
+      expectedError = testHelpers.expectXHRFailure,
+      badError = testHelpers.unexpectedXHRFailure;
 
   var controllerMock = {
     close: function(message, info) {
@@ -61,7 +65,7 @@
 
   function expectedClose(message, field, value) {
     return function(m, info) {
-      ok(m, message, "correct message: " + message);
+      equal(m, message, "correct message: " + message);
 
       if(value) {
         equal(info[field], value, field + " has correct value of " + value);
@@ -72,14 +76,6 @@
     }
   }
 
-  function badError() {
-    ok(false, "error should have never been called");
-  }
-
-  function expectedError() {
-    ok(true, "error condition expected");
-    start();
-  }
 
   function badClose() {
     ok(false, "close should have never been called");
@@ -90,10 +86,14 @@
       testHelpers.setup();
       closeCB = errorCB = null;
       errorCB = badError;
+      user.init({
+        provisioning: provisioning
+      });
     },
 
     teardown: function() {
       testHelpers.teardown();
+      user.reset();
     }
   });
 
@@ -113,10 +113,7 @@
 
     xhr.useResult("ajaxError");
     storage.addEmail("registered@testuser.com", {});
-    dialogHelpers.getAssertion.call(controllerMock, "registered@testuser.com", function() {
-      ok(false, "unexpected finish");
-      start();
-    });
+    dialogHelpers.getAssertion.call(controllerMock, "registered@testuser.com", testHelpers.unexpectedSuccess);
   });
 
   asyncTest("authenticateUser happy case", function() {
@@ -144,7 +141,18 @@
     });
   });
 
-  asyncTest("createUser happy case", function() {
+  asyncTest("createUser with known secondary, user not staged", function() {
+    closeCB = badClose;
+
+    xhr.useResult("known_secondary");
+    dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", function(staged) {
+      equal(staged, false, "user was not staged");
+      start();
+    });
+  });
+
+  asyncTest("createUser with unknown secondary happy case, expect 'user_staged' message", function() {
+    xhr.useResult("unknown_secondary");
     closeCB = expectedClose("user_staged", "email", "unregistered@testuser.com");
 
     dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
@@ -153,23 +161,45 @@
     });
   });
 
-  asyncTest("createUser could not create case", function() {
+  asyncTest("createUser with unknown secondary, user throttled", function() {
     closeCB = badClose;
 
-    xhr.useResult("invalid");
-    dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", function(staged) {
+    xhr.useResult("throttle");
+    dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
       equal(staged, false, "user was not staged");
       start();
     });
   });
 
-
   asyncTest("createUser with XHR error", function() {
     errorCB = expectedError;
 
     xhr.useResult("ajaxError");
-    dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", function(staged) {
-      ok(false, "complete should not have been called");
+    dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", testHelpers.unexpectedSuccess);
+  });
+
+  asyncTest("createUser with unknown primary, user verified - expect 'primary_user_verified' message", function() {
+    closeCB = expectedClose("primary_user_verified", "email", "unregistered@testuser.com");
+
+    xhr.useResult("primary");
+    provisioning.setSuccess(true);
+
+    dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
+      equal(staged, true, "user was staged");
+      start();
+    });
+  });
+
+  asyncTest("createUser with unknown primary, user must verify with IdP - expect 'primary_verify_user' message", function() {
+    closeCB = expectedClose("primary_verify_user", "email", "unregistered@testuser.com");
+
+    xhr.useResult("primary");
+    provisioning.setFailure({
+      code: "MUST_AUTHENTICATE"
+    });
+
+    dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
+      equal(staged, true, "user was staged");
       start();
     });
   });
diff --git a/resources/static/test/qunit/resources/state_machine_unit_test.js b/resources/static/test/qunit/resources/state_machine_unit_test.js
index e988f9351decfd052811c81832a7bac1a02329c9..9453ffb7c87c54e3e0545eeee012ca833e464cd7 100644
--- a/resources/static/test/qunit/resources/state_machine_unit_test.js
+++ b/resources/static/test/qunit/resources/state_machine_unit_test.js
@@ -102,6 +102,14 @@
 
     doError: function() {
       this.error = true;
+    },
+
+    doPrimaryUserVerified: function() {
+      this.primaryUserVerified = true;
+    },
+
+    doVerifyPrimaryUser: function() {
+      this.verifyPrimaryUser = true;
     }
   };
 
@@ -154,6 +162,18 @@
     ok(controllerMock.emailConfirmed, "user was confirmed");
   });
 
+  // XXX make these and the messages for secondary match up so there is consistency.
+  test("primary_user_verified calls doPrimaryUserVerified", function() {
+    mediator.publish("primary_user_verified");
+
+    ok(controllerMock.primaryUserVerified, "doPrimaryUserVerified called");
+  });
+
+  test("primary_verify_user calls doVerifyPrimaryUser", function() {
+    mediator.publish("primary_verify_user");
+    ok(controllerMock.verifyPrimaryUser, "doVerifyPrimaryUser called");
+  });
+
   test("authenticated", function() {
     mediator.publish("authenticated");
 
diff --git a/resources/static/test/qunit/shared/network_unit_test.js b/resources/static/test/qunit/shared/network_unit_test.js
index d8d3fa46745c3111a52e938a9058458ebe17e288..dc7d44725e217b5756ff8f2265194e7f39f46ab3 100644
--- a/resources/static/test/qunit/shared/network_unit_test.js
+++ b/resources/static/test/qunit/shared/network_unit_test.js
@@ -1,5 +1,5 @@
 /*jshint browsers:true, forin: true, laxbreak: true */
-/*global wrappedAsyncTest: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
+/*global asyncTest: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -37,22 +37,10 @@
 (function() {
   "use strict";
 
-  var testName,
-      bid = BrowserID,
+  var bid = BrowserID,
       mediator = bid.Mediator,
-      xhr = bid.Mocks.xhr;
-
-  function wrappedAsyncTest(name, test) {
-    asyncTest(name, function() {
-      testName = name;
-      test();
-    });
-  }
-
-  function wrappedStart() {
-    console.log("start: " + testName);
-    start();
-  }
+      xhr = bid.Mocks.xhr,
+      testHelpers = bid.TestHelpers;
 
   function notificationCheck(cb) {
     // Take the original arguments, take off the function.  Add any additional
@@ -70,7 +58,7 @@
       ok(info.network.type, "request type is in network info");
       equal(info.network.textStatus, "errorStatus", "textStatus is in network info");
       equal(info.network.errorThrown, "errorThrown", "errorThrown is in response info");
-      wrappedStart();
+      start();
       mediator.unsubscribe(handle);
     };
 
@@ -81,22 +69,29 @@
     }
   }
 
+  function unexpectedFailure() {
+    return function() {
+      ok(false, "unexpected failure");
+      start();
+    }
+  }
+
   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
     // onFailure to the end.  Then call the callback.
     var args = Array.prototype.slice.call(arguments, 1);
 
-    args.push(function onSuccess(authenticated) {
+    args.push(function onSuccess() {
       ok(false, "XHR failure should never pass");
-      wrappedStart();
+      start();
     }, function onFailure(info) {
       ok(true, "XHR failure should never pass");
       ok(info.network.url, "url is in network info");
       ok(info.network.type, "request type is in network info");
       equal(info.network.textStatus, "errorStatus", "textStatus is in network info");
       equal(info.network.errorThrown, "errorThrown", "errorThrown is in response info");
-      wrappedStart();
+      start();
     });
 
     xhr.useResult("ajaxError");
@@ -108,72 +103,71 @@
 
   module("shared/network", {
     setup: function() {
-      network.setXHR(xhr);
-      xhr.useResult("valid");
+      testHelpers.setup();
     },
     teardown: function() {
-      network.setXHR($);
+      testHelpers.teardown();
     }
   });
 
 
-  wrappedAsyncTest("authenticate with valid user", function() {
+  asyncTest("authenticate with valid user", function() {
     network.authenticate("testuser@testuser.com", "testuser", function onSuccess(authenticated) {
       equal(authenticated, true, "valid authentication");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "valid authentication");
-      wrappedStart();
+      start();
     });
   });
 
-  wrappedAsyncTest("authenticate with invalid user", function() {
+  asyncTest("authenticate with invalid user", function() {
     xhr.useResult("invalid");
     network.authenticate("testuser@testuser.com", "invalid", function onSuccess(authenticated) {
       equal(authenticated, false, "invalid authentication");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "invalid authentication");
-      wrappedStart();
+      start();
     });
   });
 
-  wrappedAsyncTest("authenticate with XHR failure, checking whether application is notified", function() {
+  asyncTest("authenticate with XHR failure, checking whether application is notified", function() {
     notificationCheck(network.authenticate, "testuser@testuser.com", "ajaxError");
   });
 
-  wrappedAsyncTest("authenticate with XHR failure after context already setup", function() {
+  asyncTest("authenticate with XHR failure after context already setup", function() {
     failureCheck(network.authenticate, "testuser@testuser.com", "ajaxError");
   });
 
 
-  wrappedAsyncTest("checkAuth with valid authentication", function() {
+  asyncTest("checkAuth with valid authentication", function() {
     xhr.setContextInfo("authenticated", true);
     network.checkAuth(function onSuccess(authenticated) {
       equal(authenticated, true, "we have an authentication");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "checkAuth failure");
-      wrappedStart();
+      start();
     });
   });
 
-  wrappedAsyncTest("checkAuth with invalid authentication", function() {
+  asyncTest("checkAuth with invalid authentication", function() {
     xhr.useResult("invalid");
     xhr.setContextInfo("authenticated", false);
 
     network.checkAuth(function onSuccess(authenticated) {
       equal(authenticated, false, "we are not authenticated");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "checkAuth failure");
-      wrappedStart();
+      start();
     });
   });
 
 
 
-  wrappedAsyncTest("checkAuth with XHR failure", function() {
+  asyncTest("checkAuth with XHR failure", function() {
     xhr.useResult("ajaxError");
     xhr.setContextInfo("authenticated", false);
 
@@ -182,428 +176,399 @@
     // request, we do not test whether the app is notified of an XHR failure
     network.checkAuth(function onSuccess() {
       ok(true, "checkAuth does not make an ajax call, all good");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "checkAuth does not make an ajax call, should not fail");
-      wrappedStart();
+      start();
     });
 
   });
 
 
-  wrappedAsyncTest("logout", function() {
+  asyncTest("logout", function() {
     network.logout(function onSuccess() {
       ok(true, "we can logout");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "logout failure");
-      wrappedStart();
+      start();
     });
   });
 
 
-  wrappedAsyncTest("logout with XHR failure", function() {
+  asyncTest("logout with XHR failure", function() {
     notificationCheck(network.logout);
   });
 
-  wrappedAsyncTest("logout with XHR failure", function() {
+  asyncTest("logout with XHR failure", function() {
     failureCheck(network.logout);
   });
 
 
-  wrappedAsyncTest("complete_email_addition valid", function() {
+  asyncTest("complete_email_addition valid", function() {
     network.completeEmailRegistration("goodtoken", function onSuccess(proven) {
       equal(proven, true, "good token proved");
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("complete_email_addition with invalid token", function() {
+  asyncTest("complete_email_addition with invalid token", function() {
     xhr.useResult("invalid");
     network.completeEmailRegistration("badtoken", function onSuccess(proven) {
       equal(proven, false, "bad token could not be proved");
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("complete_email_addition with XHR failure", function() {
+  asyncTest("complete_email_addition with XHR failure", function() {
     notificationCheck(network.completeEmailRegistration, "goodtoken");
   });
 
-  wrappedAsyncTest("complete_email_addition with XHR failure", function() {
+  asyncTest("complete_email_addition with XHR failure", function() {
     failureCheck(network.completeEmailRegistration, "goodtoken");
   });
 
-  wrappedAsyncTest("createUser with valid user", function() {
+  asyncTest("createUser with valid user", function() {
     network.createUser("validuser", "origin", function onSuccess(created) {
       ok(created);
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("createUser with invalid user", function() {
+  asyncTest("createUser with invalid user", function() {
     xhr.useResult("invalid");
     network.createUser("invaliduser", "origin", function onSuccess(created) {
       equal(created, false);
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("createUser throttled", function() {
+  asyncTest("createUser throttled", function() {
     xhr.useResult("throttle");
 
     network.createUser("validuser", "origin", function onSuccess(added) {
       equal(added, false, "throttled email returns onSuccess but with false as the value");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("createUser with XHR failure", function() {
+  asyncTest("createUser with XHR failure", function() {
     notificationCheck(network.createUser, "validuser", "origin");
   });
 
-  wrappedAsyncTest("createUser with XHR failure", function() {
+  asyncTest("createUser with XHR failure", function() {
     failureCheck(network.createUser, "validuser", "origin");
   });
 
-  wrappedAsyncTest("checkUserRegistration with pending email", function() {
+  asyncTest("checkUserRegistration with pending email", function() {
     xhr.useResult("pending");
 
     network.checkUserRegistration("registered@testuser.com", function(status) {
       equal(status, "pending");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("checkUserRegistration with complete email", function() {
+  asyncTest("checkUserRegistration with complete email", function() {
     xhr.useResult("complete");
 
     network.checkUserRegistration("registered@testuser.com", function(status) {
       equal(status, "complete");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("checkUserRegistration with XHR failure", function() {
+  asyncTest("checkUserRegistration with XHR failure", function() {
     notificationCheck(network.checkUserRegistration, "registered@testuser.com");
   });
 
-  wrappedAsyncTest("checkUserRegistration with XHR failure", function() {
+  asyncTest("checkUserRegistration with XHR failure", function() {
     failureCheck(network.checkUserRegistration, "registered@testuser.com");
   });
 
-  wrappedAsyncTest("completeUserRegistration with valid token", function() {
+  asyncTest("completeUserRegistration with valid token", function() {
     network.completeUserRegistration("token", "password", function(registered) {
       ok(registered);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("completeUserRegistration with invalid token", function() {
+  asyncTest("completeUserRegistration with invalid token", function() {
     xhr.useResult("invalid");
 
     network.completeUserRegistration("token", "password", function(registered) {
       equal(registered, false);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("completeUserRegistration with XHR failure", function() {
+  asyncTest("completeUserRegistration with XHR failure", function() {
     notificationCheck(network.completeUserRegistration, "token", "password");
   });
 
-  wrappedAsyncTest("completeUserRegistration with XHR failure", function() {
+  asyncTest("completeUserRegistration with XHR failure", function() {
     failureCheck(network.completeUserRegistration, "token", "password");
   });
 
-  wrappedAsyncTest("cancelUser valid", function() {
+  asyncTest("cancelUser valid", function() {
 
     network.cancelUser(function() {
       // XXX need a test here.
       ok(true);
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("cancelUser invalid", function() {
+  asyncTest("cancelUser invalid", function() {
     xhr.useResult("invalid");
 
     network.cancelUser(function() {
       // XXX need a test here.
       ok(true);
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("cancelUser with XHR failure", function() {
+  asyncTest("cancelUser with XHR failure", function() {
     notificationCheck(network.cancelUser);
   });
 
-  wrappedAsyncTest("cancelUser with XHR failure", function() {
+  asyncTest("cancelUser with XHR failure", function() {
     failureCheck(network.cancelUser);
   });
 
-  wrappedAsyncTest("emailRegistered with taken email", function() {
+  asyncTest("emailRegistered with taken email", function() {
     network.emailRegistered("registered@testuser.com", function(taken) {
       equal(taken, true, "a taken email is marked taken");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("emailRegistered with nottaken email", function() {
+  asyncTest("emailRegistered with nottaken email", function() {
     network.emailRegistered("unregistered@testuser.com", function(taken) {
       equal(taken, false, "a not taken email is not marked taken");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("emailRegistered with XHR failure", function() {
+  asyncTest("emailRegistered with XHR failure", function() {
     notificationCheck(network.emailRegistered, "registered@testuser.com");
   });
 
-  wrappedAsyncTest("emailRegistered with XHR failure", function() {
+  asyncTest("emailRegistered with XHR failure", function() {
     failureCheck(network.emailRegistered, "registered@testuser.com");
   });
 
 
-  wrappedAsyncTest("addEmail valid", function() {
+  asyncTest("addEmail valid", function() {
     network.addEmail("address", "origin", function onSuccess(added) {
       ok(added);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("addEmail invalid", function() {
+  asyncTest("addEmail invalid", function() {
     xhr.useResult("invalid");
     network.addEmail("address", "origin", function onSuccess(added) {
       equal(added, false);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("addEmail throttled", function() {
+  asyncTest("addEmail throttled", function() {
     xhr.useResult("throttle");
 
     network.addEmail("address", "origin", function onSuccess(added) {
       equal(added, false, "throttled email returns onSuccess but with false as the value");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("addEmail with XHR failure", function() {
+  asyncTest("addEmail with XHR failure", function() {
     notificationCheck(network.addEmail, "address", "origin");
   });
 
-  wrappedAsyncTest("addEmail with XHR failure", function() {
+  asyncTest("addEmail with XHR failure", function() {
     failureCheck(network.addEmail, "address", "origin");
   });
 
-  wrappedAsyncTest("checkEmailRegistration pending", function() {
+  asyncTest("checkEmailRegistration pending", function() {
     xhr.useResult("pending");
 
     network.checkEmailRegistration("registered@testuser.com", function(status) {
       equal(status, "pending");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("checkEmailRegistration complete", function() {
+  asyncTest("checkEmailRegistration complete", function() {
     xhr.useResult("complete");
 
     network.checkEmailRegistration("registered@testuser.com", function(status) {
       equal(status, "complete");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("checkEmailRegistration with XHR failure", function() {
+  asyncTest("checkEmailRegistration with XHR failure", function() {
     notificationCheck(network.checkEmailRegistration, "address");
   });
 
-  wrappedAsyncTest("checkEmailRegistration with XHR failure", function() {
+  asyncTest("checkEmailRegistration with XHR failure", function() {
     failureCheck(network.checkEmailRegistration, "address");
   });
 
 
-  wrappedAsyncTest("removeEmail valid", function() {
+  asyncTest("removeEmail valid", function() {
     network.removeEmail("validemail", function onSuccess() {
       // XXX need a test here;
       ok(true);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("removeEmail invalid", function() {
+  asyncTest("removeEmail invalid", function() {
     xhr.useResult("invalid");
 
     network.removeEmail("invalidemail", function onSuccess() {
       // XXX need a test here;
       ok(true);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("removeEmail with XHR failure", function() {
+  asyncTest("removeEmail with XHR failure", function() {
     notificationCheck(network.removeEmail, "validemail");
   });
 
-  wrappedAsyncTest("removeEmail with XHR failure", function() {
+  asyncTest("removeEmail with XHR failure", function() {
     failureCheck(network.removeEmail, "invalidemail");
   });
 
 
-  wrappedAsyncTest("requestPasswordReset", function() {
+  asyncTest("requestPasswordReset", function() {
     network.requestPasswordReset("address", "origin", function onSuccess() {
       // XXX need a test here;
       ok(true);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("requestPasswordReset with XHR failure", function() {
+  asyncTest("requestPasswordReset with XHR failure", function() {
     notificationCheck(network.requestPasswordReset, "address", "origin");
   });
 
-  wrappedAsyncTest("requestPasswordReset with XHR failure", function() {
+  asyncTest("requestPasswordReset with XHR failure", function() {
     failureCheck(network.requestPasswordReset, "address", "origin");
   });
 
-  wrappedAsyncTest("resetPassword", function() {
+  asyncTest("resetPassword", function() {
     network.resetPassword("password", function onSuccess() {
       // XXX need a test here;
       ok(true);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("resetPassword with XHR failure", function() {
+  asyncTest("resetPassword with XHR failure", function() {
     xhr.useResult("ajaxError");
 /*
     the body of this function is not yet written
 
     network.resetPassword("password", function onSuccess() {
       ok(false, "XHR failure should never call success");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(true, "XHR failure should always call failure");
-      wrappedStart();
+      start();
     });
 */
     start();
   });
 
-  wrappedAsyncTest("changePassword", function() {
-    network.changePassword("oldpassword", "newpassword", function onSuccess() {
-      // XXX need a real wrappedAsyncTest here.
-      ok(true);
-      wrappedStart();
-    }, function onFailure() {
-      ok(false);
-      wrappedStart();
-    });
-
-  });
-
-  wrappedAsyncTest("changePassword with XHR failure", function() {
-    xhr.useResult("ajaxError");
-
-    /*
-    the body of this function is not yet written.
-    network.changePassword("oldpassword", "newpassword", function onSuccess() {
-      ok(false, "XHR failure should never call success");
-      wrappedStart();
-    }, function onFailure() {
-      ok(true, "XHR failure should always call failure");
-      wrappedStart();
-    });
-
-    */
-    start();
-  });
-
-  wrappedAsyncTest("serverTime", function() {
+  asyncTest("serverTime", function() {
     // I am forcing the server time to be 1.25 seconds off.
     xhr.setContextInfo("server_time", new Date().getTime() - 1250);
     network.serverTime(function onSuccess(time) {
@@ -613,53 +578,122 @@
       // time as it is on the server, which could be more than 100ms off of
       // what the local machine says it is.
       //equal(Math.abs(diff) < 100, true, "server time and local time should be less than 100ms different (is " + diff + "ms different)");
-      wrappedStart();
+      start();
     }, function onfailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("serverTime with XHR failure before context has been setup", function() {
+  asyncTest("serverTime with XHR failure before context has been setup", function() {
     notificationCheck();
     xhr.useResult("contextAjaxError");
 
     network.serverTime();
   });
 
-  wrappedAsyncTest("serverTime with XHR failure before context has been setup", function() {
+  asyncTest("serverTime with XHR failure before context has been setup", function() {
     xhr.useResult("contextAjaxError");
 
     network.serverTime(function onSuccess(time) {
       ok(false, "XHR failure should never call success");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(true, "XHR failure should always call failure");
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("codeVersion", function() {
+  asyncTest("codeVersion", function() {
     network.codeVersion(function onComplete(version) {
       equal(version, "ABC123", "version returned properly");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "unexpected failure");
-      wrappedStart();
+      start();
     });
   });
 
-  wrappedAsyncTest("codeVersion with XHR error", function() {
+  asyncTest("codeVersion with XHR error", function() {
     xhr.useResult("contextAjaxError");
 
     network.codeVersion(function onComplete(version) {
       ok(false, "XHR failure should never call complete");
-      wrappedStart();
+      start();
     }, function onFailure() {
-      ok(true, "XHR fialure should always return failure");
-      wrappedStart();
+      ok(true, "XHR failure should always return failure");
+      start();
     });
+  });
+
+  asyncTest("addressInfo with unknown secondary email", function() {
+    xhr.useResult("unknown_secondary");
 
+    network.addressInfo("testuser@testuser.com", function onComplete(data) {
+      equal(data.type, "secondary", "type is secondary");
+      equal(data.known, false, "address is unknown to BrowserID");
+      start();
+    }, unexpectedFailure);
+  });
+
+  asyncTest("addressInfo with known seconday email", function() {
+    xhr.useResult("known_secondary");
+
+    network.addressInfo("testuser@testuser.com", function onComplete(data) {
+      equal(data.type, "secondary", "type is secondary");
+      equal(data.known, true, "address is known to BrowserID");
+      start();
+    }, unexpectedFailure);
+  });
+
+  asyncTest("addressInfo with primary email", function() {
+    xhr.useResult("primary");
+
+    network.addressInfo("testuser@testuser.com", function onComplete(data) {
+      equal(data.type, "primary", "type is primary");
+      ok("auth" in data, "auth field exists");
+      ok("prov" in data, "prov field exists");
+      start();
+    }, unexpectedFailure);
+  });
+
+  asyncTest("addressInfo with XHR error", function() {
+    xhr.useResult("ajaxError");
+    failureCheck(network.addressInfo, "testuser@testuser.com");
+  });
+
+  asyncTest("changePassword happy case, expect complete callback with true status", function() {
+    network.changePassword("oldpassword", "newpassword", function onComplete(status) {
+      equal(status, true, "calls onComplete with true status");
+      start();
+    }, function onFailure() {
+      ok(false, "unexpected failure");
+      start();
+    });
+  });
+
+  asyncTest("changePassword with incorrect old password, expect complete callback with false status", function() {
+    xhr.useResult("incorrectPassword");
+
+    network.changePassword("oldpassword", "newpassword", function onComplete(status) {
+      equal(status, false, "calls onComplete with false status");
+      start();
+    }, function onFailure() {
+      ok(false, "unexpected failure");
+      start();
+    });
+  });
+
+  asyncTest("changePassword with XHR error, expect error callback", function() {
+    xhr.useResult("ajaxError");
+
+    network.changePassword("oldpassword", "newpassword", function onComplete() {
+      ok(false, "XHR failure should never call complete");
+      start();
+    }, function onFailure() {
+      ok(true, "XHR failure should always call failure");
+      start();
+    });
   });
 }());
diff --git a/resources/static/test/qunit/shared/tooltip_unit_test.js b/resources/static/test/qunit/shared/tooltip_unit_test.js
index 6c5d7e359aa030f9675044e89f58296794d22bb5..1c3bb4065c284e2c291e615155157e235cfdea0e 100644
--- a/resources/static/test/qunit/shared/tooltip_unit_test.js
+++ b/resources/static/test/qunit/shared/tooltip_unit_test.js
@@ -76,4 +76,15 @@
     });
   });
 
+  asyncTest("show tooltip, then reset - hides tooltip, resets shown status", function() {
+    tooltip.showTooltip("#shortTooltip");
+    setTimeout(function() {
+      tooltip.reset();
+
+      equal($(".tooltip:visible").length, 0, "after reset, all tooltips are hidden");
+      equal(tooltip.shown, false, "after reset, tooltip status is reset");
+      start();
+    }, 100);
+  });
+
 }());
diff --git a/resources/static/test/qunit/shared/user_unit_test.js b/resources/static/test/qunit/shared/user_unit_test.js
index e90fd1c32e4a82ffb971c2538566a94fe5ff710c..997c7239960f445f3d66f4f7789125de7a629f9b 100644
--- a/resources/static/test/qunit/shared/user_unit_test.js
+++ b/resources/static/test/qunit/shared/user_unit_test.js
@@ -44,7 +44,9 @@ var jwcert = require("./jwcert");
       storage = bid.Storage,
       network = bid.Network,
       xhr = bid.Mocks.xhr,
-      testOrigin = "testOrigin";
+      testOrigin = "https://browserid.org",
+      testHelpers = bid.TestHelpers,
+      provisioning = bid.Mocks.Provisioning
 
   // I generated these locally, they are used nowhere else.
   var pubkey = {"algorithm":"RS","n":"56063028070432982322087418176876748072035482898334811368408525596198252519267108132604198004792849077868951906170812540713982954653810539949384712773390200791949565903439521424909576832418890819204354729217207360105906039023299561374098942789996780102073071760852841068989860403431737480182725853899733706069","e":"65537"};
@@ -92,14 +94,11 @@ var jwcert = require("./jwcert");
 
   module("shared/user", {
     setup: function() {
-      network.setXHR(xhr);
-      xhr.useResult("valid");
-      lib.clearStoredEmailKeypairs();
+      testHelpers.setup();
       lib.setOrigin(testOrigin);
-      storage.site.remove(testOrigin, "email");
     },
     teardown: function() {
-      network.setXHR($);
+      testHelpers.teardown();
     }
   });
 
@@ -116,11 +115,11 @@ var jwcert = require("./jwcert");
   });
 
   test("setOrigin, getHostname", function() {
-    var origin = "http://testorigin.com:10001";
+    var origin = "http://browserid.org";
     lib.setOrigin(origin);
 
     var hostname = lib.getHostname();
-    equal(hostname, "testorigin.com", "getHostname returns only the hostname");
+    equal(hostname, "browserid.org", "getHostname returns only the hostname");
   });
 
   test("getStoredEmailKeypairs", function() {
@@ -156,6 +155,7 @@ var jwcert = require("./jwcert");
     equal(0, count, "after clearing, there are no identities");
   });
 
+  /*
   asyncTest("createUser", function() {
     lib.createUser("testuser@testuser.com", function(status) {
       ok(status, "user created");
@@ -183,6 +183,70 @@ var jwcert = require("./jwcert");
       start();
     });
   });
+*/
+  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();
+    }, failure("createUser failure"));
+  });
+
+  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();
+    }, failure("createUser failure"));
+  });
+
+  asyncTest("createUser with unknown secondary, XHR failure - expect failure call", function() {
+    xhr.useResult("ajaxError");
+
+    lib.createUser("unregistered@testuser.com",
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectXHRFailure
+    );
+  });
+
+  asyncTest("createUser with primary, user verified with primary - expect 'primary.verified'", function() {
+    xhr.useResult("primary");
+    provisioning.setSuccess(true);
+
+    lib.createUser("unregistered@testuser.com", function(status) {
+      equal(status, "primary.verified", "primary user is already verified, correct status");
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("createUser with primary, user must authenticate with primary - expect 'primary.verify'", function() {
+    xhr.useResult("primary");
+
+    provisioning.setFailure({
+      code: "MUST_AUTHENTICATE",
+      msg: "Wahhooo!!"
+    });
+
+    lib.createUser("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() {
+    xhr.useResult("primary");
+    provisioning.setFailure({
+      code: "primaryError",
+      msg: "some error"
+    });
+
+    lib.createUser("unregistered@testuser.com",
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectXHRFailure
+    );
+  });
 
   asyncTest("waitForUserValidation with `complete` response", function() {
     storage.setStagedOnBehalfOf(testOrigin);
diff --git a/resources/static/test/qunit/testHelpers/helpers.js b/resources/static/test/qunit/testHelpers/helpers.js
index d7ca0c676660680ba8737649b1fa02ba827b4cfc..61890240ceafa51f74c34371e1cf8bbd859d0c04 100644
--- a/resources/static/test/qunit/testHelpers/helpers.js
+++ b/resources/static/test/qunit/testHelpers/helpers.js
@@ -2,11 +2,15 @@
   var bid = BrowserID,
       mediator = bid.Mediator,
       network = bid.Network,
+      user = bid.User,
       storage = bid.Storage,
       xhr = bid.Mocks.xhr,
+      provisioning = bid.Mocks.Provisioning,
       screens = bid.Screens,
+      tooltip = bid.Tooltip,
       registrations = [];
-      calls = {};
+      calls = {},
+      testOrigin = "https://browserid.org";
 
   function register(message, cb) {
     registrations.push(mediator.subscribe(message, function(msg, info) {
@@ -43,12 +47,20 @@
       var el = $("#controller_head");
       el.find("#formWrap .contents").html("");
       el.find("#wait .contents").html("");
-      $("#error").html("<div class='contents'></div>").hide();
-
+      $(".error").removeClass("error");
+      $("#error").stop().html("<div class='contents'></div>").hide();
+      $(".notification").stop().hide();
       unregisterAll();
       mediator.reset();
       screens.wait.hide();
       screens.error.hide();
+      tooltip.reset();
+      provisioning.setSuccess(false);
+      provisioning.setFailure(false);
+      user.init({
+        provisioning: provisioning
+      });
+      user.setOrigin(testOrigin);
     },
 
     teardown: function() {
@@ -56,15 +68,38 @@
       mediator.reset();
       network.setXHR($);
       storage.clear();
-      $("#error").html("<div class='contents'></div>").hide();
+      $(".error").removeClass("error");
+      $("#error").stop().html("<div class='contents'></div>").hide();
+      $(".notification").stop().hide();
       screens.wait.hide();
       screens.error.hide();
+      tooltip.reset();
+      provisioning.setSuccess(false);
+      provisioning.setFailure(false);
+      user.reset();
     },
 
     register: register,
     errorVisible: function() {
       return screens.error.visible;
     },
-    checkNetworkError: checkNetworkError
+    testErrorVisible: function() {
+      equal(this.errorVisible(), true, "error screen is visible");
+    },
+    checkNetworkError: checkNetworkError,
+    unexpectedSuccess: function() {
+      ok(false, "unexpected success");
+      start();
+    },
+
+    expectXHRFailure: function() {
+      ok(true, "expected XHR failure");
+      start();
+    },
+
+    unexpectedXHRFailure: function() {
+      ok(false, "unexpected XHR failure");
+      start();
+    }
   };
 }());
diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs
index fb7412ef13a561e9eefa87e4dd3cc1f29e042749..52fa963ec241e7d4763f6b181a4940da0f1aba5f 100644
--- a/resources/views/dialog_layout.ejs
+++ b/resources/views/dialog_layout.ejs
@@ -91,6 +91,7 @@
           <script type="text/javascript" src="/dialog/controllers/pickemail.js"></script>
           <script type="text/javascript" src="/dialog/controllers/addemail.js"></script>
           <script type="text/javascript" src="/dialog/controllers/required_email.js"></script>
+          <script type="text/javascript" src="/dialog/controllers/verify_primary_user.js"></script>
           <script type="text/javascript" src="/dialog/start.js"></script>
         <% } %>
       <% } %>
diff --git a/resources/views/index.ejs b/resources/views/index.ejs
index 46cae5c26fa0a3bfe6856940f8437bff1614f065..9d9aea739112578bb45d68478ad9551920e336e3 100644
--- a/resources/views/index.ejs
+++ b/resources/views/index.ejs
@@ -5,15 +5,45 @@
 
       <div id="manage">
           <h1 class="serif">Account Manager</h1>
-          <div class="buttonrow cf">
-              <strong>Your Email Addresses</strong>
-
-              <button id="manageAccounts" href="#">edit</button>
-              <button id="cancelManage" href="#">done</button>
-          </div>
-          <ul id="emailList">
-          </ul>
-          <div id="disclaimer">You may, at any time, <a href="#" id="cancelAccount">cancel your account</a></div>
+
+          <section>
+            <header class="buttonrow cf">
+                <h2>Your Email Addresses</h2>
+
+                <button class="edit">edit</button>
+                <button class="done">done</button>
+            </header>
+
+            <ul id="emailList">
+            </ul>
+          </section>
+
+          <section id="edit_password">
+            <header class="buttonrow cf">
+              <h2>Password</h2>
+
+              <button class="edit">edit</button>
+              <button class="done">cancel</button>
+            </header>
+
+            <div class="showedit">
+              <label for="old_password">Old Password</label>
+              <label for="new_password">New Password</label>
+            </div>
+
+            <form id="edit_password_form" class="showedit">
+              <input type="password" id="old_password" name="old_password" placeholder="old password"/>
+              <input type="password" id="new_password" name="new_password" placeholder="new password"/>
+              <button id="changePassword">done</button>
+
+              <div class="tooltip" for="old_password" id="tooltipOldRequired">Old password is required</div>
+              <div class="tooltip" for="old_password" id="tooltipInvalidPassword">Incorrect old password, password not updated</div>
+              <div class="tooltip" for="new_password" id="tooltipNewRequired">New password is required</div>
+            </form>
+          </section>
+
+
+          <p id="disclaimer">You may, at any time, <a href="#" id="cancelAccount">cancel your account</a></p>
       </div>
   </div>
 
diff --git a/resources/views/signup.ejs b/resources/views/signup.ejs
index 1578755fa2ee71c89e922758b348b32cb5e96e8b..26997d3c19ce02eb7168f3e8cf07bb93157c27fe 100644
--- a/resources/views/signup.ejs
+++ b/resources/views/signup.ejs
@@ -27,6 +27,15 @@
                   </p>
                 </li>
 
+                <li class="notification" id="congrats">
+                    <p class="serif">
+                        <strong id="email">Your address</strong> has been verified!
+
+                        <p class="siteinfo">
+                          Your new address is set up!
+                        </p>
+                    </p>
+                </li>
             </ul>
 
             <ul class="inputs forminputs">