diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js
index 0cc46aab5a3dadfbe852c2ddf458b2550815fc89..b64dcbf422e5a04a4ecfadbc109b859116f6d266 100644
--- a/resources/static/dialog/controllers/dialog.js
+++ b/resources/static/dialog/controllers/dialog.js
@@ -84,16 +84,19 @@ BrowserID.Modules.Dialog = (function() {
     if (win.location.hash == "#NATIVE" || win.location.hash == "#INTERNAL") {
       // don't do winchan, let it be.
       return;
-    }      
+    }
 
     try {
-      //
       WinChan.onOpen(function(origin, args, cb) {
-        self.get(origin, args.params, function(r) {
-          cb(r);
-        }, function (e) {
-          cb(null);
-        });
+        // XXX this is called whenever the primary provisioning iframe gets
+        // added.  If there are no args, then do not do self.get.
+        if(args) {
+          self.get(origin, args.params, function(r) {
+            cb(r);
+          }, function (e) {
+            cb(null);
+          });
+        }
       });
     } catch (e) {
       self.renderError("error", {
diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js
index 4449bc51858fa427a4545521683c5e46efcf56b5..28d45ca23144a02566e23ce0b3f4096347528790 100644
--- a/resources/static/dialog/resources/helpers.js
+++ b/resources/static/dialog/resources/helpers.js
@@ -96,7 +96,7 @@
     }
 
     var self=this;
-    user.createUser(email, function(status) {
+    user.createUser(email, function(status, info) {
       switch(status) {
         case "secondary.already_added":
           // XXX how to handle this - createUser should not be called on
@@ -125,7 +125,8 @@
           break;
         case "primary.verify":
           self.close("primary_verify_user", {
-            email: email
+            email: email,
+            auth_url: info.auth
           });
           complete(true);
           break;
diff --git a/resources/static/pages/signup.js b/resources/static/pages/signup.js
index a70e72fc2b558cf97f595e7b8fb060e43e0e5a51..0613649254d78d265dd70e627e5db3cfd5b44bdb 100644
--- a/resources/static/pages/signup.js
+++ b/resources/static/pages/signup.js
@@ -46,12 +46,25 @@ BrowserID.signUp = (function() {
       errors = bid.Errors,
       tooltip = BrowserID.Tooltip,
       ANIMATION_SPEED = 250,
-      storedEmail = pageHelpers;
+      storedEmail = pageHelpers,
+      win = window,
+      verifyEmail,
+      verifyURL;
 
     function showNotice(selector) {
       $(selector).fadeIn(ANIMATION_SPEED);
     }
 
+    function verifyWithPrimary(oncomplete) {
+      if(!(verifyEmail && verifyURL)) {
+        throw "cannot verify with primary without an email address and URL"
+      }
+
+      var url = verifyURL + "?email=" + encodeURIComponent(verifyEmail);
+      win.open(url, "_moz_primary_verification", "width: 500px, height: 500px");
+      oncomplete && oncomplete();
+    }
+
     function submit(oncomplete) {
       var email = helpers.getAndValidateEmail("#email");
 
@@ -60,7 +73,7 @@ BrowserID.signUp = (function() {
       }
 
       if (email) {
-        user.createUser(email, function onComplete(status) {
+        user.createUser(email, function onComplete(status, info) {
           switch(status) {
             case "secondary.already_added":
               $('#registeredEmail').html(email);
@@ -81,8 +94,10 @@ BrowserID.signUp = (function() {
               pageHelpers.replaceInputsWithNotice("#congrats", complete.bind(null, true));
               break;
             case "primary.verify":
-              // XXX What do we do here?
-              complete(false);
+              verifyEmail = email;
+              verifyURL = info.auth;
+              dom.setInner("#primary_email", email);
+              pageHelpers.replaceInputsWithNotice("#primary_verify", complete.bind(null, false));
               break;
             case "primary.could_not_add":
               // XXX Can this happen?
@@ -108,6 +123,10 @@ BrowserID.signUp = (function() {
     function init(config) {
       config = config || {};
 
+      if(config.window) {
+        win = config.window;
+      }
+
       $("form input[autofocus]").focus();
 
       pageHelpers.setupEmail();
@@ -115,6 +134,7 @@ BrowserID.signUp = (function() {
       dom.bindEvent("#email", "keyup", onEmailKeyUp);
       dom.bindEvent("form", "submit", cancelEvent(submit));
       dom.bindEvent("#back", "click", cancelEvent(back));
+      dom.bindEvent("#verifyWithPrimary", "click", cancelEvent(verifyWithPrimary));
     }
 
     // BEGIN TESTING API
@@ -122,11 +142,15 @@ BrowserID.signUp = (function() {
       dom.unbindEvent("#email", "keyup");
       dom.unbindEvent("form", "submit");
       dom.unbindEvent("#back", "click");
+      dom.unbindEvent("#verifyWithPrimary", "click");
+      win = window;
+      verifyEmail = verifyURL = null;
     }
 
     init.submit = submit;
     init.reset = reset;
     init.back = back;
+    init.verifyWithPrimary = verifyWithPrimary;
     // END TESTING API
 
     return init;
diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js
index 16c0f60b82cedfe6118b6b9370f1129fb0ac9e84..f253372cdd281e14e17a53fd10ab7834fb373779 100644
--- a/resources/static/shared/network.js
+++ b/resources/static/shared/network.js
@@ -149,6 +149,24 @@ BrowserID.Network = (function() {
     csrf_token = server_time = auth_status = undef;
   }
 
+  function handleAuthenticationResponse(onComplete, onFailure, status) {
+    if (onComplete) {
+      try {
+        var authenticated = status.success;
+
+        if (typeof authenticated !== 'boolean') throw status;
+
+        // at this point we know the authentication status of the
+        // session, let's set it to perhaps save a network request
+        // (to fetch session context).
+        auth_status = authenticated;
+        if (onComplete) onComplete(authenticated);
+      } catch (e) {
+        onFailure("unexpected server response: " + e);
+      }
+    }
+  }
+
   // Not really part of the Network API, but related to networking
   $(document).bind("offline", function() {
     mediator.publish("offline");
@@ -170,33 +188,39 @@ BrowserID.Network = (function() {
      * @method authenticate
      * @param {string} email - address to authenticate
      * @param {string} password - password.
-     * @param {function} [onSuccess] - callback to call for success
+     * @param {function} [onComplete] - callback to call when complete.  Called
+     * with status parameter - true if authenticated, false otw.
      * @param {function} [onFailure] - called on XHR failure
      */
-    authenticate: function(email, password, onSuccess, onFailure) {
+    authenticate: function(email, password, onComplete, onFailure) {
       post({
         url: "/wsapi/authenticate_user",
         data: {
           email: email,
           pass: password
         },
-        success: function(status, textStatus, jqXHR) {
-          if (onSuccess) {
-            try {
-              var authenticated = status.success;
-
-              if (typeof authenticated !== 'boolean') throw status;
-
-              // at this point we know the authentication status of the
-              // session, let's set it to perhaps save a network request
-              // (to fetch session context).
-              auth_status = authenticated;
-              if (onSuccess) onSuccess(authenticated);
-            } catch (e) {
-              onFailure("unexpected server response: " + e);
-            }
-          }
+        success: handleAuthenticationResponse.bind(null, onComplete, onFailure),
+        error: onFailure
+      });
+    },
+
+    /**
+     * Authenticate with a primary generated assertion
+     * @method authenticateWithAssertion
+     * @param {string} email - address to authenticate
+     * @param {string} assertion
+     * @param {function} [onComplete] - callback to call when complete.  Called
+     * with status parameter - true if authenticated, false otw.
+     * @param {function} [onFailure] - called on XHR failure
+     */
+    authenticateWithAssertion: function(email, assertion, onComplete, onFailure) {
+      post({
+        url: "/wsapi/auth_with_assertion",
+        data: {
+          email: email,
+          assertion: assertion
         },
+        success: handleAuthenticationResponse.bind(null, onComplete, onFailure),
         error: onFailure
       });
     },
@@ -204,15 +228,15 @@ BrowserID.Network = (function() {
     /**
      * Check whether a user is currently logged in.
      * @method checkAuth
-     * @param {function} [onSuccess] - Success callback, called with one
+     * @param {function} [onComplete] - called with one
      * boolean parameter, whether the user is authenticated.
      * @param {function} [onFailure] - called on XHR failure.
      */
-    checkAuth: function(onSuccess, onFailure) {
+    checkAuth: function(onComplete, onFailure) {
       withContext(function() {
         try {
           if (typeof auth_status !== 'boolean') throw "can't get authentication status!";
-          if (onSuccess) onSuccess(auth_status);
+          if (onComplete) onComplete(auth_status);
         } catch(e) {
           if (onFailure) onFailure(e.toString());
         }
@@ -222,10 +246,10 @@ BrowserID.Network = (function() {
     /**
      * Log the authenticated user out
      * @method logout
-     * @param {function} [onSuccess] - called on completion
+     * @param {function} [onComplete] - called on completion
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    logout: function(onSuccess, onFailure) {
+    logout: function(onComplete, onFailure) {
       post({
         url: "/wsapi/logout",
         success: function() {
@@ -235,7 +259,7 @@ BrowserID.Network = (function() {
           // FIXME: we should return a confirmation that the
           // user was successfully logged out.
           auth_status = false;
-          if (onSuccess) onSuccess();
+          if (onComplete) onComplete();
         },
         error: onFailure
       });
@@ -246,10 +270,10 @@ BrowserID.Network = (function() {
      * @method createUser
      * @param {string} email - Email address to prepare.
      * @param {string} origin - site user is trying to sign in to.
-     * @param {function} [onSuccess] - Callback to call when complete.
+     * @param {function} [onComplete] - Callback to call when complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    createUser: function(email, origin, onSuccess, onFailure) {
+    createUser: function(email, origin, onComplete, onFailure) {
       post({
         url: "/wsapi/stage_user",
         data: {
@@ -257,12 +281,12 @@ BrowserID.Network = (function() {
           site : origin
         },
         success: function(status) {
-          if (onSuccess) onSuccess(status.success);
+          if (onComplete) onComplete(status.success);
         },
         error: function(info) {
           // 403 is throttling.
           if (info.network.status === 403) {
-            if (onSuccess) onSuccess(false);
+            if (onComplete) onComplete(false);
           }
           else if (onFailure) onFailure(info);
         }
@@ -277,11 +301,11 @@ BrowserID.Network = (function() {
      * TODO: think about whether this requires the right cookie
      * I think so (BA).
      */
-    emailForVerificationToken: function(token, onSuccess, onFailure) {
+    emailForVerificationToken: function(token, onComplete, onFailure) {
       get({
         url : "/wsapi/email_for_token?token=" + encodeURIComponent(token),
         success: function(data) {
-          if (onSuccess) onSuccess(data.email);
+          if (onComplete) onComplete(data.email);
         },
         error: onFailure
       });
@@ -290,14 +314,14 @@ BrowserID.Network = (function() {
     /**
      * Check the current user"s registration status
      * @method checkUserRegistration
-     * @param {function} [onSuccess] - Called when complete.
+     * @param {function} [onComplete] - Called when complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    checkUserRegistration: function(email, onSuccess, onFailure) {
+    checkUserRegistration: function(email, onComplete, onFailure) {
       get({
         url: "/wsapi/user_creation_status?email=" + encodeURIComponent(email),
         success: function(status, textStatus, jqXHR) {
-          if (onSuccess) onSuccess(status.status);
+          if (onComplete) onComplete(status.status);
         },
         error: onFailure
       });
@@ -308,10 +332,10 @@ BrowserID.Network = (function() {
      * @method completeUserRegistration
      * @param {string} token - token to register for.
      * @param {string} password - password to register for account.
-     * @param {function} [onSuccess] - Called when complete.
+     * @param {function} [onComplete] - Called when complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    completeUserRegistration: function(token, password, onSuccess, onFailure) {
+    completeUserRegistration: function(token, password, onComplete, onFailure) {
       post({
         url: "/wsapi/complete_user_creation",
         data: {
@@ -319,7 +343,7 @@ BrowserID.Network = (function() {
           pass: password
         },
         success: function(status, textStatus, jqXHR) {
-          if (onSuccess) onSuccess(status.success);
+          if (onComplete) onComplete(status.success);
         },
         error: onFailure
       });
@@ -329,18 +353,18 @@ BrowserID.Network = (function() {
      * Call with a token to prove an email address ownership.
      * @method completeEmailRegistration
      * @param {string} token - token proving email ownership.
-     * @param {function} [onSuccess] - Callback to call when complete.  Called
+     * @param {function} [onComplete] - Callback to call when complete.  Called
      * with one boolean parameter that specifies the validity of the token.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    completeEmailRegistration: function(token, onSuccess, onFailure) {
+    completeEmailRegistration: function(token, onComplete, onFailure) {
       post({
         url: "/wsapi/complete_email_addition",
         data: {
           token: token
         },
         success: function(status, textStatus, jqXHR) {
-          if (onSuccess) onSuccess(status.success);
+          if (onComplete) onComplete(status.success);
         },
         error: onFailure
       });
@@ -350,12 +374,12 @@ BrowserID.Network = (function() {
      * Request a password reset for the given email address.
      * @method requestPasswordReset
      * @param {string} email - email address to reset password for.
-     * @param {function} [onSuccess] - Callback to call when complete.
+     * @param {function} [onComplete] - Callback to call when complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    requestPasswordReset: function(email, origin, onSuccess, onFailure) {
+    requestPasswordReset: function(email, origin, onComplete, onFailure) {
       if (email) {
-        Network.createUser(email, origin, onSuccess, onFailure);
+        Network.createUser(email, origin, onComplete, onFailure);
       } else {
         // TODO: if no email is provided, then what?
         throw "no email provided to password reset";
@@ -366,12 +390,12 @@ BrowserID.Network = (function() {
      * Update the password of the current user. This is for a password reseT
      * @method resetPassword
      * @param {string} password - new password.
-     * @param {function} [onSuccess] - Callback to call when complete.
+     * @param {function} [onComplete] - Callback to call when complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    resetPassword: function(password, onSuccess, onFailure) {
+    resetPassword: function(password, onComplete, onFailure) {
       // XXX fill this in.
-      if (onSuccess) onSuccess();
+      if (onComplete) onComplete();
     },
 
     /**
@@ -390,8 +414,8 @@ BrowserID.Network = (function() {
           oldpass: oldPassword,
           newpass: newPassword
         },
-        success: function(response) {
-          if (onComplete) onComplete(response.success);
+        success: function(status) {
+          if (onComplete) onComplete(status.success);
         },
         error: onFailure
       });
@@ -401,13 +425,13 @@ BrowserID.Network = (function() {
     /**
      * Cancel the current user"s account.
      * @method cancelUser
-     * @param {function} [onSuccess] - called whenever complete.
+     * @param {function} [onComplete] - called whenever complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    cancelUser: function(onSuccess, onFailure) {
+    cancelUser: function(onComplete, onFailure) {
       post({
         url: "/wsapi/account_cancel",
-        success: onSuccess,
+        success: onComplete,
         error: onFailure
       });
     },
@@ -417,10 +441,10 @@ BrowserID.Network = (function() {
      * @method addEmail
      * @param {string} email - Email address to add.
      * @param {string} origin - site user is trying to sign in to.
-     * @param {function} [onsuccess] - called when complete.
-     * @param {function} [onfailure] - called on xhr failure.
+     * @param {function} [onComplete] - called when complete.
+     * @param {function} [onFailure] - called on xhr failure.
      */
-    addEmail: function(email, origin, onSuccess, onFailure) {
+    addEmail: function(email, origin, onComplete, onFailure) {
       post({
         url: "/wsapi/stage_email",
         data: {
@@ -428,12 +452,12 @@ BrowserID.Network = (function() {
           site: origin
         },
         success: function(response) {
-          if (onSuccess) onSuccess(response.success);
+          if (onComplete) onComplete(response.success);
         },
         error: function(info) {
           // 403 is throttling.
           if (info.network.status === 403) {
-            if (onSuccess) onSuccess(false);
+            if (onComplete) onComplete(false);
           }
           else if (onFailure) onFailure(info);
         }
@@ -447,11 +471,11 @@ BrowserID.Network = (function() {
      * @param {function} [onsuccess] - called when complete.
      * @param {function} [onfailure] - called on xhr failure.
      */
-    checkEmailRegistration: function(email, onSuccess, onFailure) {
+    checkEmailRegistration: function(email, onComplete, onFailure) {
       get({
         url: "/wsapi/email_addition_status?email=" + encodeURIComponent(email),
         success: function(status, textStatus, jqXHR) {
-          if (onSuccess) onSuccess(status.status);
+          if (onComplete) onComplete(status.status);
         },
         error: onFailure
       });
@@ -461,16 +485,16 @@ BrowserID.Network = (function() {
      * Check whether the email is already registered.
      * @method emailRegistered
      * @param {string} email - Email address to check.
-     * @param {function} [onSuccess] - Called with one boolean parameter when
+     * @param {function} [onComplete] - Called with one boolean parameter when
      * complete.  Parameter is true if `email` is already registered, false
      * otw.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    emailRegistered: function(email, onSuccess, onFailure) {
+    emailRegistered: function(email, onComplete, onFailure) {
       get({
         url: "/wsapi/have_email?email=" + encodeURIComponent(email),
         success: function(data, textStatus, xhr) {
-          if (onSuccess) onSuccess(data.email_known);
+          if (onComplete) onComplete(data.email_known);
         },
         error: onFailure
       });
@@ -503,17 +527,17 @@ BrowserID.Network = (function() {
      * Remove an email address from the current user.
      * @method removeEmail
      * @param {string} email - Email address to remove.
-     * @param {function} [onSuccess] - Called whenever complete.
+     * @param {function} [onComplete] - Called whenever complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    removeEmail: function(email, onSuccess, onFailure) {
+    removeEmail: function(email, onComplete, onFailure) {
       post({
         url: "/wsapi/remove_email",
         data: {
           email: email
         },
         success: function(status, textStatus, jqXHR) {
-          if (onSuccess) onSuccess(status.success);
+          if (onComplete) onComplete(status.success);
         },
         error: onFailure
       });
@@ -523,14 +547,14 @@ BrowserID.Network = (function() {
      * Certify the public key for the email address.
      * @method certKey
      */
-    certKey: function(email, pubkey, onSuccess, onFailure) {
+    certKey: function(email, pubkey, onComplete, onFailure) {
       post({
         url: "/wsapi/cert_key",
         data: {
           email: email,
           pubkey: pubkey.serialize()
         },
-        success: onSuccess,
+        success: onComplete,
         error: onFailure
       });
     },
@@ -539,10 +563,10 @@ BrowserID.Network = (function() {
      * List emails
      * @method listEmails
      */
-    listEmails: function(onSuccess, onFailure) {
+    listEmails: function(onComplete, onFailure) {
       get({
         url: "/wsapi/list_emails",
-        success: onSuccess,
+        success: onComplete,
         error: onFailure
       });
     },
@@ -557,12 +581,12 @@ BrowserID.Network = (function() {
      *
      * @method serverTime
      */
-    serverTime: function(onSuccess, onFailure) {
+    serverTime: function(onComplete, onFailure) {
       withContext(function() {
         try {
           if (!server_time) throw "can't get server time!";
           var offset = (new Date()).getTime() - server_time.local;
-          if (onSuccess) onSuccess(new Date(offset + server_time.remote));
+          if (onComplete) onComplete(new Date(offset + server_time.remote));
         } catch(e) {
           if (onFailure) onFailure(e.toString());
         }
@@ -578,11 +602,11 @@ BrowserID.Network = (function() {
      *
      * @method domainKeyCreationTime
      */
-    domainKeyCreationTime: function(onSuccess, onFailure) {
+    domainKeyCreationTime: function(onComplete, onFailure) {
       withContext(function() {
         try {
           if (!domain_key_creation_time) throw "can't get domain key creation time!";
-          if (onSuccess) onSuccess(new Date(domain_key_creation_time));
+          if (onComplete) onComplete(new Date(domain_key_creation_time));
         } catch(e) {
           if (onFailure) onFailure(e.toString());
         }
diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js
index 46a24d10a0bdefd87604d8a6966f88e17cb43a6f..10011b41daff13f5bfd30147b746b963593b2fad 100644
--- a/resources/static/shared/user.js
+++ b/resources/static/shared/user.js
@@ -296,9 +296,22 @@ BrowserID.User = (function() {
     },
 
     /**
-     * Status:
-     * "already_added", "verify_secondary", "secondary_could_not_add", "verify_primary",
-     * "primary_verified"
+     * Create a user.  Works for both primaries and secondaries.
+     * @method createUser
+     * @param {string} email
+     * @param {function} onComplete - function to call on complettion.  Called
+     * with two parameters - status and info.
+     * Status can be:
+     *  secondary.already_added
+     *  secondary.verify
+     *  secondary.could_not_add
+     *  primary.already_added
+     *  primary.verified
+     *  primary.verify
+     *  primary.could_not_add
+     *
+     *  info is passed on primary.verify and contains the info necessary to
+     *  verify the user with the IdP
      */
     createUser: function(email, onComplete, onFailure) {
       var self=this;
@@ -324,13 +337,24 @@ BrowserID.User = (function() {
         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");
+        }, function(keypair, cert) {
+          persistEmailKeypair(email, "primary", keypair, cert, function() {
+            User.getAssertion(email, function(assertion) {
+              if(assertion) {
+                network.authenticateWithAssertion(email, assertion, function(status) {
+                  var message = status ? "primary.verified" : "primary.could_not_add";
+                  onComplete(message);
+                }, onFailure);
+              }
+              else {
+                // XXX perhaps these failure modes should call onFailure instead.
+                onComplete("primary.could_not_add");
+              }
+            }, onFailure);
+          }, onFailure);
+        }, function(error) {
+          if(error.code === "primaryError" && error.msg === "user is not authenticated as target user") {
+            onComplete("primary.verify", info);
           }
           else {
             onFailure(info);
diff --git a/resources/static/test/index.html b/resources/static/test/index.html
index d209296ed3065a4c1e2606a7f3b4cddb1d9a16c8..9a990769264f68e87b75949ca961509d33681b81 100644
--- a/resources/static/test/index.html
+++ b/resources/static/test/index.html
@@ -65,6 +65,7 @@
       <li class="notification emailsent">Email Sent</li>
       <li class="notification doh">doh</li>
       <li class="notification" id="congrats">Congratulations!</li>
+      <li class="notification" id="primary_verify"><span id="primary_email"></span></li>
     </ul>
 
     <ul id="emailList">
diff --git a/resources/static/test/qunit/mocks/provisioning.js b/resources/static/test/qunit/mocks/provisioning.js
index 70a8760e627c3de8546e748ee46cde2b4accd39f..b83bde1b4a7f0e6d8a52b445a78042f0626b5ac7 100644
--- a/resources/static/test/qunit/mocks/provisioning.js
+++ b/resources/static/test/qunit/mocks/provisioning.js
@@ -35,18 +35,45 @@
  *
  * ***** END LICENSE BLOCK ***** */
 BrowserID.Mocks.Provisioning = (function() {
+
+  "use strict";
+
+  var keypair,
+      // this cert is meaningless, but it has the right format
+      cert = "eyJhbGciOiJSUzEyOCJ9.eyJpc3MiOiJpc3N1ZXIuY29tIiwiZXhwIjoxMzE2Njk1MzY3NzA3LCJwdWJsaWMta2V5Ijp7ImFsZ29yaXRobSI6IlJTIiwibiI6IjU2MDYzMDI4MDcwNDMyOTgyMzIyMDg3NDE4MTc2ODc2NzQ4MDcyMDM1NDgyODk4MzM0ODExMzY4NDA4NTI1NTk2MTk4MjUyNTE5MjY3MTA4MTMyNjA0MTk4MDA0NzkyODQ5MDc3ODY4OTUxOTA2MTcwODEyNTQwNzEzOTgyOTU0NjUzODEwNTM5OTQ5Mzg0NzEyNzczMzkwMjAwNzkxOTQ5NTY1OTAzNDM5NTIxNDI0OTA5NTc2ODMyNDE4ODkwODE5MjA0MzU0NzI5MjE3MjA3MzYwMTA1OTA2MDM5MDIzMjk5NTYxMzc0MDk4OTQyNzg5OTk2NzgwMTAyMDczMDcxNzYwODUyODQxMDY4OTg5ODYwNDAzNDMxNzM3NDgwMTgyNzI1ODUzODk5NzMzNzA2MDY5IiwiZSI6IjY1NTM3In0sInByaW5jaXBhbCI6eyJlbWFpbCI6InRlc3R1c2VyQHRlc3R1c2VyLmNvbSJ9fQ.aVIO470S_DkcaddQgFUXciGwq2F_MTdYOJtVnEYShni7I6mqBwK3fkdWShPEgLFWUSlVUtcy61FkDnq2G-6ikSx1fUZY7iBeSCOKYlh6Kj9v43JX-uhctRSB2pI17g09EUtvmb845EHUJuoowdBLmLa4DSTdZE-h4xUQ9MsY7Ik",
+      failure,
+      jwk = require("./jwk"),
+      status;
+
   function Provisioning(info, onsuccess, onfailure) {
-    if(Provisioning.failure) onfailure(Provisioning.failure);
-    else onsuccess();
+    if(status === Provisioning.AUTHENTICATED) {
+      onsuccess(keypair, cert);
+    }
+    else onfailure(failure);
   }
 
-  Provisioning.setSuccess = function(status) {
-    Provisioning.status = status;
+  Provisioning.setStatus = function(newStatus) {
+    failure = null;
+
+    if(newStatus === Provisioning.NOT_AUTHENTICATED) {
+      failure = {
+        code: "primaryError",
+        msg: "user is not authenticated as target user"
+      };
+    }
+    else if(newStatus === Provisioning.AUTHENTICATED) {
+      keypair = keypair || jwk.KeyPair.generate("DS", 256);
+    }
+
+    status = newStatus;
   };
 
+  Provisioning.NOT_AUTHENTICATED = "not_authenticated";
+  Provisioning.AUTHENTICATED = "authenticated";
+
   Provisioning.setFailure = function(status) {
-    Provisioning.failure = status;
-  }
+    failure = status;
+  };
 
   return Provisioning;
 }());
diff --git a/resources/static/test/qunit/mocks/xhr.js b/resources/static/test/qunit/mocks/xhr.js
index e2f48b980ff484c45934263f4f03b531e89cbc7f..bc309a62750f292feb944fc8f4b55145b52fbb67 100644
--- a/resources/static/test/qunit/mocks/xhr.js
+++ b/resources/static/test/qunit/mocks/xhr.js
@@ -64,6 +64,10 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/authenticate_user valid": { success: true },
       "post /wsapi/authenticate_user invalid": { success: false },
       "post /wsapi/authenticate_user ajaxError": undefined,
+      "post /wsapi/auth_with_assertion primary": { success: true },
+      "post /wsapi/auth_with_assertion valid": { success: true },
+      "post /wsapi/auth_with_assertion invalid": { success: false },
+      "post /wsapi/auth_with_assertion ajaxError": undefined,
       "post /wsapi/cert_key valid": random_cert,
       "post /wsapi/cert_key invalid": undefined,
       "post /wsapi/cert_key ajaxError": undefined,
@@ -118,10 +122,10 @@ BrowserID.Mocks.xhr = (function() {
       "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=unregistered%40testuser.com primary": { type: "primary", auth: "https://auth_url", prov: "https://prov_url" },
       "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 primary": { type: "primary", auth: "https://auth_url", prov: "https://prov_url" },
       "get /wsapi/address_info?email=testuser%40testuser.com ajaxError": undefined
     },
 
diff --git a/resources/static/test/qunit/pages/signup_unit_test.js b/resources/static/test/qunit/pages/signup_unit_test.js
index b55022f0ad9df19b5fab4bbe3041618b34997a46..08efadc86917f5688d6953a92a610d8bee239d0f 100644
--- a/resources/static/test/qunit/pages/signup_unit_test.js
+++ b/resources/static/test/qunit/pages/signup_unit_test.js
@@ -40,14 +40,31 @@
   var bid = BrowserID,
       network = bid.Network,
       xhr = bid.Mocks.xhr,
-      testOrigin = "http://browserid.org",
       testHelpers = bid.TestHelpers,
-      provisioning = bid.Mocks.Provisioning;
+      provisioning = bid.Mocks.Provisioning,
+      win;
+
+  function DocumentMock() {
+    this.location = document.location;
+  }
+
+  function WindowMock() {
+    this.document = new DocumentMock();
+  }
+  WindowMock.prototype = {
+    open: function(url, name, options) {
+      this.open_url = url;
+    }
+  };
 
   module("pages/signup", {
     setup: function() {
       testHelpers.setup();
-      bid.signUp();
+
+      win = new WindowMock();
+      bid.signUp({
+        window: win
+      });
     },
     teardown: function() {
       testHelpers.teardown();
@@ -133,7 +150,6 @@
 
   asyncTest("signup with primary email address, provisioning failure - expect error screen", function() {
     xhr.useResult("primary");
-
     $("#email").val("unregistered@testuser.com");
     provisioning.setFailure({
       code: "internal",
@@ -149,33 +165,38 @@
 
   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);
+    provisioning.setStatus(provisioning.AUTHENTICATED);
 
     bid.signUp.submit(function(status) {
       equal(status, true, "primary addition success - true status");
-      equal($(".notification:visible").length, 1, "success notification is visible");
+      equal($("#congrats: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() {
+  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($("#primary_verify:visible").length, 1, "success notification is visible");
+      equal($("#primary_email").text(), "unregistered@testuser.com", "correct email shown");
       equal(status, false, "user must authenticate, some action needed.");
       start();
     });
   });
 
+  asyncTest("verifyWithPrimary opens new tab", function() {
+    xhr.useResult("primary");
+    $("#email").val("unregistered@testuser.com");
+
+    bid.signUp.submit(function(status) {
+      bid.signUp.verifyWithPrimary(function() {
+        equal(win.open_url, "https://auth_url?email=unregistered%40testuser.com", "user directed to authentication URL");
+        start();
+      });
+    });
+  });
+
 }());
diff --git a/resources/static/test/qunit/resources/helpers_unit_test.js b/resources/static/test/qunit/resources/helpers_unit_test.js
index 66613847707e62012465aa9686671ff3980a5edc..62e6e486a55d6755468d87286f928164ba70962a 100644
--- a/resources/static/test/qunit/resources/helpers_unit_test.js
+++ b/resources/static/test/qunit/resources/helpers_unit_test.js
@@ -48,7 +48,7 @@
       provisioning = bid.Mocks.Provisioning,
       closeCB,
       errorCB,
-      expectedError = testHelpers.expectXHRFailure,
+      expectedError = testHelpers.expectedXHRFailure,
       badError = testHelpers.unexpectedXHRFailure;
 
   var controllerMock = {
@@ -182,7 +182,7 @@
     closeCB = expectedClose("primary_user_verified", "email", "unregistered@testuser.com");
 
     xhr.useResult("primary");
-    provisioning.setSuccess(true);
+    provisioning.setStatus(provisioning.AUTHENTICATED);
 
     dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
       equal(staged, true, "user was staged");
@@ -192,11 +192,7 @@
 
   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");
diff --git a/resources/static/test/qunit/shared/network_unit_test.js b/resources/static/test/qunit/shared/network_unit_test.js
index dc7d44725e217b5756ff8f2265194e7f39f46ab3..b89ec7b5a4d0767552108e9188e78c1a384043f7 100644
--- a/resources/static/test/qunit/shared/network_unit_test.js
+++ b/resources/static/test/qunit/shared/network_unit_test.js
@@ -40,7 +40,8 @@
   var bid = BrowserID,
       mediator = bid.Mediator,
       xhr = bid.Mocks.xhr,
-      testHelpers = bid.TestHelpers;
+      testHelpers = bid.TestHelpers,
+      TEST_EMAIL = "testuser@testuser.com";
 
   function notificationCheck(cb) {
     // Take the original arguments, take off the function.  Add any additional
@@ -112,7 +113,7 @@
 
 
   asyncTest("authenticate with valid user", function() {
-    network.authenticate("testuser@testuser.com", "testuser", function onSuccess(authenticated) {
+    network.authenticate(TEST_EMAIL, "testuser", function onSuccess(authenticated) {
       equal(authenticated, true, "valid authentication");
       start();
     }, function onFailure() {
@@ -123,7 +124,7 @@
 
   asyncTest("authenticate with invalid user", function() {
     xhr.useResult("invalid");
-    network.authenticate("testuser@testuser.com", "invalid", function onSuccess(authenticated) {
+    network.authenticate(TEST_EMAIL, "invalid", function onSuccess(authenticated) {
       equal(authenticated, false, "invalid authentication");
       start();
     }, function onFailure() {
@@ -133,11 +134,38 @@
   });
 
   asyncTest("authenticate with XHR failure, checking whether application is notified", function() {
-    notificationCheck(network.authenticate, "testuser@testuser.com", "ajaxError");
+    notificationCheck(network.authenticate, TEST_EMAIL, "ajaxError");
   });
 
   asyncTest("authenticate with XHR failure after context already setup", function() {
-    failureCheck(network.authenticate, "testuser@testuser.com", "ajaxError");
+    failureCheck(network.authenticate, TEST_EMAIL, "ajaxError");
+  });
+
+  asyncTest("authenticateWithAssertion with valid email/assertioni, returns true status", function() {
+    network.authenticateWithAssertion(TEST_EMAIL, "test_assertion", function(status) {
+      equal(status, true, "user authenticated, status set to true");
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("authenticateWithAssertion with invalid email/assertion", function() {
+    xhr.useResult("invalid");
+
+    network.authenticateWithAssertion(TEST_EMAIL, "test_assertion", function(status) {
+      equal(status, false, "user not authenticated, status set to false");
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("authenticateWithAssertion with XHR error", function() {
+    xhr.useResult("ajaxError");
+
+    network.authenticateWithAssertion(
+      TEST_EMAIL,
+      "test_assertion",
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectedXHRFailure
+    );
   });
 
 
@@ -146,10 +174,7 @@
     network.checkAuth(function onSuccess(authenticated) {
       equal(authenticated, true, "we have an authentication");
       start();
-    }, function onFailure() {
-      ok(false, "checkAuth failure");
-      start();
-    });
+    }, testHelpers.unexpectedXHRFailure);
   });
 
   asyncTest("checkAuth with invalid authentication", function() {
@@ -159,10 +184,7 @@
     network.checkAuth(function onSuccess(authenticated) {
       equal(authenticated, false, "we are not authenticated");
       start();
-    }, function onFailure() {
-      ok(false, "checkAuth failure");
-      start();
-    });
+    }, testHelpers.unexpectedXHRFailure);
   });
 
 
@@ -630,7 +652,7 @@
   asyncTest("addressInfo with unknown secondary email", function() {
     xhr.useResult("unknown_secondary");
 
-    network.addressInfo("testuser@testuser.com", function onComplete(data) {
+    network.addressInfo(TEST_EMAIL, function onComplete(data) {
       equal(data.type, "secondary", "type is secondary");
       equal(data.known, false, "address is unknown to BrowserID");
       start();
@@ -640,7 +662,7 @@
   asyncTest("addressInfo with known seconday email", function() {
     xhr.useResult("known_secondary");
 
-    network.addressInfo("testuser@testuser.com", function onComplete(data) {
+    network.addressInfo(TEST_EMAIL, function onComplete(data) {
       equal(data.type, "secondary", "type is secondary");
       equal(data.known, true, "address is known to BrowserID");
       start();
@@ -650,7 +672,7 @@
   asyncTest("addressInfo with primary email", function() {
     xhr.useResult("primary");
 
-    network.addressInfo("testuser@testuser.com", function onComplete(data) {
+    network.addressInfo(TEST_EMAIL, function onComplete(data) {
       equal(data.type, "primary", "type is primary");
       ok("auth" in data, "auth field exists");
       ok("prov" in data, "prov field exists");
@@ -660,7 +682,7 @@
 
   asyncTest("addressInfo with XHR error", function() {
     xhr.useResult("ajaxError");
-    failureCheck(network.addressInfo, "testuser@testuser.com");
+    failureCheck(network.addressInfo, TEST_EMAIL);
   });
 
   asyncTest("changePassword happy case, expect complete callback with true status", function() {
@@ -696,4 +718,5 @@
       start();
     });
   });
+
 }());
diff --git a/resources/static/test/qunit/shared/user_unit_test.js b/resources/static/test/qunit/shared/user_unit_test.js
index 997c7239960f445f3d66f4f7789125de7a629f9b..eb0e5c3c3726c9bb79aa169238d5a9af7f4eb682 100644
--- a/resources/static/test/qunit/shared/user_unit_test.js
+++ b/resources/static/test/qunit/shared/user_unit_test.js
@@ -155,35 +155,32 @@ var jwcert = require("./jwcert");
     equal(0, count, "after clearing, there are no identities");
   });
 
-  /*
-  asyncTest("createUser", function() {
-    lib.createUser("testuser@testuser.com", function(status) {
+  asyncTest("createSecondaryUser", function() {
+    lib.createSecondaryUser("testuser@testuser.com", function(status) {
       ok(status, "user created");
       start();
-    }, failure("createUser failure"));
+    }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("createUser with user creation refused", function() {
+  asyncTest("createSecondaryUser with user creation refused", function() {
     xhr.useResult("throttle");
 
-    lib.createUser("testuser@testuser.com", function(status) {
+    lib.createSecondaryUser("testuser@testuser.com", function(status) {
       equal(status, false, "user creation refused");
       start();
-    }, failure("createUser failure"));
+    }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("createUser with XHR failure", function() {
+  asyncTest("createSecondaryUser with XHR failure", function() {
     xhr.useResult("ajaxError");
 
-    lib.createUser("testuser@testuser.com", function(status) {
-      ok(false, "xhr failure should never succeed");
-      start();
-    }, function() {
-      ok(true, "xhr failure should always be a failure");
-      start();
-    });
+    lib.createSecondaryUser(
+      "testuser@testuser.com",
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectedXHRFailure
+    );
   });
-*/
+
   asyncTest("createUser with unknown secondary happy case - expect 'secondary.verify'", function() {
     xhr.useResult("unknown_secondary");
 
@@ -207,28 +204,26 @@ var jwcert = require("./jwcert");
 
     lib.createUser("unregistered@testuser.com",
       testHelpers.unexpectedSuccess,
-      testHelpers.expectXHRFailure
+      testHelpers.expectedXHRFailure
     );
   });
 
   asyncTest("createUser with primary, user verified with primary - expect 'primary.verified'", function() {
     xhr.useResult("primary");
-    provisioning.setSuccess(true);
+    provisioning.setStatus(provisioning.AUTHENTICATED);
 
     lib.createUser("unregistered@testuser.com", function(status) {
       equal(status, "primary.verified", "primary user is already verified, correct status");
-      start();
+      network.checkAuth(function(authenticated) {
+        equal(authenticated, true, "after provisioning user, user should be automatically authenticated to BrowserID");
+        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();
@@ -244,7 +239,7 @@ var jwcert = require("./jwcert");
 
     lib.createUser("unregistered@testuser.com",
       testHelpers.unexpectedSuccess,
-      testHelpers.expectXHRFailure
+      testHelpers.expectedXHRFailure
     );
   });
 
@@ -339,9 +334,7 @@ var jwcert = require("./jwcert");
     xhr.useResult("invalid");
 
     lib.verifyUser("token", "password", function onSuccess(info) {
-
       equal(info.valid, false, "bad token calls onSuccess with a false validity");
-
       start();
     }, failure("verifyUser failure"));
   });
diff --git a/resources/static/test/qunit/testHelpers/helpers.js b/resources/static/test/qunit/testHelpers/helpers.js
index 61890240ceafa51f74c34371e1cf8bbd859d0c04..4e27eaae5451c9f02da19802a361156c53c7733b 100644
--- a/resources/static/test/qunit/testHelpers/helpers.js
+++ b/resources/static/test/qunit/testHelpers/helpers.js
@@ -55,8 +55,7 @@
       screens.wait.hide();
       screens.error.hide();
       tooltip.reset();
-      provisioning.setSuccess(false);
-      provisioning.setFailure(false);
+      provisioning.setStatus(provisioning.NOT_AUTHENTICATED);
       user.init({
         provisioning: provisioning
       });
@@ -74,8 +73,7 @@
       screens.wait.hide();
       screens.error.hide();
       tooltip.reset();
-      provisioning.setSuccess(false);
-      provisioning.setFailure(false);
+      provisioning.setStatus(provisioning.NOT_AUTHENTICATED);
       user.reset();
     },
 
@@ -92,7 +90,7 @@
       start();
     },
 
-    expectXHRFailure: function() {
+    expectedXHRFailure: function() {
       ok(true, "expected XHR failure");
       start();
     },
diff --git a/resources/views/signup.ejs b/resources/views/signup.ejs
index 738b16335b3f6128d9bd1f2545403b624ad03c49..9f7b2b9ec17fdf3b55a0bf174c3a70784c954596 100644
--- a/resources/views/signup.ejs
+++ b/resources/views/signup.ejs
@@ -36,6 +36,17 @@
                         </p>
                     </p>
                 </li>
+
+                <li class="notification" id="primary_verify">
+                  <p>
+                    To verify that you own <strong id="primary_email">address</strong>, you must
+                    sign in with your provider.  A new window will be opened.
+                  </p>
+
+                  <p>
+                    <button id="verifyWithPrimary">Verify</button>
+                  </p>
+                </li>
             </ul>
 
             <ul class="inputs forminputs">