diff --git a/resources/static/dialog/controllers/authenticate_controller.js b/resources/static/dialog/controllers/authenticate_controller.js
index 7233670142d9dd6c95d2c4dc02de87b2683832be..380fa48ed9e5224b005d2b30a71ccec2bcfba81c 100644
--- a/resources/static/dialog/controllers/authenticate_controller.js
+++ b/resources/static/dialog/controllers/authenticate_controller.js
@@ -91,23 +91,18 @@
 
   function authenticate(el, event) {
     var email = getEmail(),
-        pass = dom.getInner("#password"),
+        pass = helpers.getAndValidatePassword("#password"),
         self = this;
 
     cancelEvent(event);
 
-    if (!validation.emailAndPassword(email, pass)) return;
-
-    user.authenticate(email, pass,
-      function onComplete(authenticated) {
-        if (authenticated) {
-          self.close("authenticated", {
-            email: email
-          });
-        } else {
-          bid.Tooltip.showTooltip("#cannot_authenticate");
-        }
-      }, self.getErrorDialog(errors.authenticate));
+    if(email && pass) {
+      helpers.authenticateUser.call(self, email, pass, function() {
+        self.close("authenticated", {
+          email: email
+        });
+      });
+    }
   }
 
   function resetPassword(el, event) {
diff --git a/resources/static/dialog/controllers/dialog_controller.js b/resources/static/dialog/controllers/dialog_controller.js
index c69e8a8e172ffbc1adc4105611b7eb4270fe57fc..d84c20a488d83b6db9a47e3da32b10f984da4dd7 100644
--- a/resources/static/dialog/controllers/dialog_controller.js
+++ b/resources/static/dialog/controllers/dialog_controller.js
@@ -61,7 +61,7 @@
 
         options = options || {};
 
-        if(options.window) {
+        if (options.window) {
           win = options.window;
         }
 
@@ -104,9 +104,10 @@
           params = {};
         }
         
-        self.allowPersistent = (params.persistent == true);
+        self.allowPersistent = !!params.persistent;
+        self.requiredEmail = params.requiredEmail;
 
-        if('onLine' in navigator && !navigator.onLine) {
+        if ('onLine' in navigator && !navigator.onLine) {
           self.doOffline();
           return;
         }
@@ -241,6 +242,10 @@
         this.element.authenticate(info);
       },
 
+      doAuthenticateWithRequiredEmail: function(info) {
+        this.element.requiredemail(info);
+      },
+
       doForgotPassword: function(email) {
         this.element.forgotpassword({
           email: email
@@ -290,13 +295,18 @@
         var self=this;
         user.checkAuthenticationAndSync(function onSuccess() {},
           function onComplete(authenticated) {
-            if (authenticated) {
+            if (self.requiredEmail) {
+              self.doAuthenticateWithRequiredEmail({
+                email: self.requiredEmail,
+                authenticated: authenticated
+              });
+            }
+            else if (authenticated) {
               self.doPickEmail();
             } else {
               self.doAuthenticate();
             }
-          },
-          self.getErrorDialog(errors.checkAuthentication));
+          }, self.getErrorDialog(errors.checkAuthentication));
     }
 
   });
diff --git a/resources/static/dialog/controllers/page_controller.js b/resources/static/dialog/controllers/page_controller.js
index ed4a4fdaaf4e5f4866863574c417aece5f62012e..8634c4a6721be594a4ae0ce0f859505e042cb369 100644
--- a/resources/static/dialog/controllers/page_controller.js
+++ b/resources/static/dialog/controllers/page_controller.js
@@ -48,39 +48,40 @@
     init: function(el, options) {
       options = options || {};
 
-      var me=this,
-          bodyTemplate = options.bodyTemplate,
-          bodyVars = options.bodyVars,
-          errorTemplate = options.errorTemplate,
-          errorVars = options.errorVars,
-          waitTemplate = options.waitTemplate,
-          waitVars = options.waitVars;
-
+      var self=this;
 
-      if(bodyTemplate) {
-        me.renderDialog(bodyTemplate, bodyVars);
+      if(options.bodyTemplate) {
+        self.renderDialog(options.bodyTemplate, options.bodyVars);
       }
 
-      if(waitTemplate) {
-        me.renderWait(waitTemplate, waitVars);
+      if(options.waitTemplate) {
+        self.renderWait(options.waitTemplate, options.waitVars);
       }
 
-      if(errorTemplate) {
-        me.renderError(errorTemplate, errorVars);
+      if(options.errorTemplate) {
+        self.renderError(options.errorTemplate, options.errorVars);
       }
 
+      self.start(options);
+    },
+
+    start: function() {
+      var self=this;
       // XXX move all of these, bleck.
-      dom.bindEvent("form", "submit", me.onSubmit.bind(me));
-      dom.bindEvent("#thisIsNotMe", "click", me.close.bind(me, "notme"));
+      dom.bindEvent("form", "submit", self.onSubmit.bind(self));
+      dom.bindEvent("#thisIsNotMe", "click", self.close.bind(self, "notme"));
     },
 
-    destroy: function() {
+    stop: function() {
       dom.unbindEvent("form", "submit");
       dom.unbindEvent("input", "keyup");
       dom.unbindEvent("#thisIsNotMe", "click");
 
       dom.removeClass("body", "waiting");
+    },
 
+    destroy: function() {
+      this.stop();
       this._super();
     },
 
diff --git a/resources/static/dialog/controllers/pickemail_controller.js b/resources/static/dialog/controllers/pickemail_controller.js
index 3a79c113ce0c4f9d1283c5e68db9a3288ac30046..c11253376646e6191085c949567e6df05dc044b5 100644
--- a/resources/static/dialog/controllers/pickemail_controller.js
+++ b/resources/static/dialog/controllers/pickemail_controller.js
@@ -45,7 +45,6 @@
       helpers = bid.Helpers,
       dom = bid.DOM,
       body = $("body"),
-      animationComplete = body.innerWidth() < 640,
       assertion;
 
   function animateSwap(fadeOutSelector, fadeInSelector, callback) {
@@ -97,41 +96,6 @@
     return !!identity;
   }
 
-  function tryClose() {
-    if (typeof assertion !== "undefined" && animationComplete) {
-      this.close("assertion_generated", {
-        assertion: assertion
-      });
-    }
-  }
-
-  function getAssertion(email) {
-    // Kick of the assertion fetching/keypair generating while we are showing
-    // the animation, hopefully this minimizes the delay the user notices.
-    var self=this;
-    user.getAssertion(email, function(assert) {
-      assertion = assert || null;
-      startAnimation.call(self);
-    }, self.getErrorDialog(errors.getAssertion));
-  }
-
-  function startAnimation() {
-    var self=this;
-    if (!animationComplete) {
-      $("#signIn").animate({"width" : "685px"}, "slow", function () {
-        // post animation
-         body.delay(500).animate({ "opacity" : "0.5"}, "fast", function () {
-           animationComplete = true;
-           tryClose.call(self);
-         });
-      });
-    }
-    else {
-      tryClose.call(self);
-    }
-
-  }
-
   function signIn(element, event) {
     cancelEvent(event);
     var self=this,
@@ -146,7 +110,7 @@
         storage.site.set(origin, "remember", $("#remember").is(":checked"));
       }
 
-      getAssertion.call(self, email);
+      helpers.getAssertion.call(self, email);
     }
   }
 
diff --git a/resources/static/dialog/controllers/required_email_controller.js b/resources/static/dialog/controllers/required_email_controller.js
new file mode 100644
index 0000000000000000000000000000000000000000..3cb3dc26db70fed0dd58f608fb780bce7c724361
--- /dev/null
+++ b/resources/static/dialog/controllers/required_email_controller.js
@@ -0,0 +1,142 @@
+/*jshint brgwser: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 ***** */
+(function() {
+  "use strict";
+
+  var ANIMATION_TIME = 250,
+      bid = BrowserID,
+      user = bid.User,
+      errors = bid.Errors,
+      helpers = bid.Helpers,
+      dom = bid.DOM,
+      assertion;
+
+  function signIn(event) {
+    event && event.preventDefault();
+
+    var self = this,
+        email = self.email;
+
+    // If the user is already authenticated and they own this address, sign 
+    // them right in.
+    if(self.authenticated) {
+      helpers.getAssertion.call(self, email);
+    }
+    else {
+      // If the user is not already authenticated, but they potentially own 
+      // this address, try and sign them in and generate an assertion if they 
+      // get the password right.
+      var password = helpers.getAndValidatePassword("#password");
+      if (password) {
+        helpers.authenticateUser.call(self, email, password, function() {
+          // Now that the user has authenticated, sync their emails and get an 
+          // assertion for the email we care about.
+          user.syncEmailKeypair(email, function() {
+            helpers.getAssertion.call(self, email);
+          }, self.getErrorDialog(errors.syncEmailKeypair));
+        });
+      }
+    }
+  }
+
+  function verifyAddress(event) {
+    event.preventDefault();
+
+  }
+
+  function forgotPassword(event) {
+    event.preventDefault();
+  }
+
+
+  PageController.extend("Requiredemail", {}, {
+    start: function(options) {
+      var self=this,
+          email = options.email || "",
+          authenticated = options.authenticated || false;
+
+      self.email = email;
+      self.authenticated = authenticated;
+
+      // NOTE: When the app first starts and the user's authentication is 
+      // checked, all email addresses for authenticated users are synced.  We 
+      // can be assured by this point that our addresses are up to date.
+      if(authenticated) {
+        // if the current user owns the required email, sign in with it 
+        // (without a password). Otherwise, make the user verify the address
+        // (which shows no password).
+        var userOwnsEmail = !!user.getStoredEmailKeypair(email);
+        showTemplate(userOwnsEmail, false);
+      }
+      else {
+        user.isEmailRegistered(email, function(registered) {
+          // If the current email address is registered but the user is not 
+          // authenticated, make them sign in with it.  Otherwise, make them 
+          // verify ownership of the address.
+          showTemplate(registered, registered);
+        }, self.getErrorDialog(errors.isEmailRegistered));
+      }
+
+      function showTemplate(requireSignin, showPassword) {
+        self.renderDialog("requiredemail", {
+          email: email,
+          signin: requireSignin,
+          showPassword: showPassword
+        });
+
+        dom.bindEvent("#sign_in", "click", signIn.bind(self));
+        dom.bindEvent("#verify_address", "click", verifyAddress.bind(self));
+        dom.bindEvent("#forgotPassword", "click", forgotPassword.bind(self));
+      }
+
+      self._super();
+    },
+
+    stop: function() {
+      dom.unbindEvent("#sign_in", "click");
+      dom.unbindEvent("#verify_address", "click");
+      dom.unbindEvent("#forgotPassword", "click");
+
+      this._super();
+    },
+
+    signIn: signIn,
+    verifyAddress: verifyAddress,
+    forgotPassword: forgotPassword
+  });
+
+}());
diff --git a/resources/static/dialog/dialog.js b/resources/static/dialog/dialog.js
index 6dd10f88ae3d5ee098d620a31554016abc603dc1..46beb215dc08ac90235dbb0472657e0411158200 100644
--- a/resources/static/dialog/dialog.js
+++ b/resources/static/dialog/dialog.js
@@ -61,19 +61,22 @@ steal
                '../shared/screens',
                '../shared/tooltip',
                '../shared/validation',
-               '../shared/helpers',
-               '../shared/browser-support',
-               '../shared/browserid-extensions',
                '../shared/network',
                '../shared/user',
                '../shared/error-messages',
-               '../shared/wait-messages')
+               '../shared/browser-support',
+               '../shared/browserid-extensions',
+               '../shared/wait-messages',
+               '../shared/helpers'
+               )
 
 	.controllers('page',
                'dialog',
                'authenticate',
                'checkregistration',
-               'pickemail')					// loads files in controllers folder
+               'pickemail',
+               'required_email'
+               )					// loads files in controllers folder
 
   .then(function() {
     $(function() {
diff --git a/resources/static/dialog/views/requiredemail.ejs b/resources/static/dialog/views/requiredemail.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..f1741c602bc81b23c8565d19503600c5a3031133
--- /dev/null
+++ b/resources/static/dialog/views/requiredemail.ejs
@@ -0,0 +1,41 @@
+  <!--strong>Required Email Sign In</strong-->
+  <div class="form_section">
+      <ul class="inputs">
+
+          The site requested you sign in with
+          <p>
+            <span id="required_email"><%= email %></span>
+          </p>
+
+          <% if (showPassword) { %>
+            <li id="password_section">
+
+                <label for="password" class="half serif">Password</label>
+                <div class="half right">
+                    <a id="forgotPassword" href="#" tabindex="4">forgot your password?</a>
+                </div>
+                <input id="password" class="sans" type="password" maxlength="80" tabindex="2" />
+
+
+                <div id="password_required" class="tooltip" for="password">
+                  The password field is required.
+                </div>
+
+                <div id="cannot_authenticate" class="tooltip" for="password">
+                  The account cannot be logged in with this username and password.
+                </div>
+            </li>
+          <% } %>
+      
+      </ul>
+
+      <div class="submit cf">
+          <% if (signin) { %>
+            <button id="sign_in" tabindex="3">sign in</button>
+          <% } else { %>
+            <button id="verify_address" tabindex="3">verify email</button>
+          <% } %>
+
+          <button id="cancel_stage" tabindex="4">cancel</button>
+      </div>
+  </div>
diff --git a/resources/static/shared/error-messages.js b/resources/static/shared/error-messages.js
index 0b760950f62b26b666ac3f85e30a0059b514131a..38c321996f8aec42f1a911f57e0110bc88453907 100644
--- a/resources/static/shared/error-messages.js
+++ b/resources/static/shared/error-messages.js
@@ -111,6 +111,10 @@ BrowserID.Errors = (function(){
       title: "Syncing Email Addresses"
     },
 
+    syncEmailKeypair: {
+      title: "Sync Keys for Address"
+    },
+
     xhrError: {
       title: "Communication Error"
     }
diff --git a/resources/static/shared/helpers.js b/resources/static/shared/helpers.js
index fa4d5bd0fa0afd17c0c2a2cbfc1e01f1b461462c..4deabe88ceb5cfadb03c8ede9cad1c49a31fb121 100644
--- a/resources/static/shared/helpers.js
+++ b/resources/static/shared/helpers.js
@@ -38,6 +38,8 @@
 
   var bid = BrowserID,
       dom = bid.DOM,
+      user = bid.User,
+      errors = bid.Errors,
       validation = bid.Validation,
       helpers = bid.Helpers = bid.Helpers || {};
 
@@ -63,6 +65,53 @@
     return password;
   }
 
+  /**
+   * XXX add a test for the next two and move them into a dialog specific 
+   * helper module!
+   */
+  function animateClose(callback) {
+    var body = $("body"),
+        doAnimation = $("#signIn").length && body.innerWidth() > 640;
+
+    if (doAnimation) {
+      $("#signIn").animate({"width" : "685px"}, "slow", function () {
+        // post animation
+         body.delay(500).animate({ "opacity" : "0.5"}, "fast", function () {
+           callback();
+         });
+      });
+    }
+    else {
+      callback();
+    }
+  }
+
+  // XXX Move this to a dialog specific module
+  function getAssertion(email) {
+    var self=this;
+    user.getAssertion(email, function(assert) {
+      assert = assert || null;
+      animateClose(function() {
+        self.close("assertion_generated", {
+          assertion: assert
+        });
+      });
+    }, self.getErrorDialog(errors.getAssertion));
+  }
+
+  // XXX Move this to a dialog specific module
+  function authenticateUser(email, pass, callback) {
+    var self=this;
+    user.authenticate(email, pass,
+      function onComplete(authenticated) {
+        if (authenticated) {
+          callback();
+        } else {
+          bid.Tooltip.showTooltip("#cannot_authenticate");
+        }
+      }, self.getErrorDialog(errors.authenticate));
+  }
+
   extend(helpers, {
     /**
      * Extend an object with the properties of another object.  Overwrites 
@@ -88,6 +137,9 @@
      * @return {string} password if password is valid, null otw.
      */
     getAndValidatePassword: getAndValidatePassword,
+
+    getAssertion: getAssertion,
+    authenticateUser: authenticateUser
   });
 
 
diff --git a/resources/static/test/qunit/controllers/dialog_controller_unit_test.js b/resources/static/test/qunit/controllers/dialog_controller_unit_test.js
index fa602bbc490831ace4460a91c039473173ebe83a..a0cf4452dfa394ede27f3f88a4a66a8e7bf79e9b 100644
--- a/resources/static/test/qunit/controllers/dialog_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/dialog_controller_unit_test.js
@@ -50,20 +50,21 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
     channelError = false;
   }
 
-  function initController() {
-    controller = el.dialog({
+  function initController(config) {
+    var config = $.extend(config, {
       window: {
         setupChannel: function() {
           if (channelError) throw "Channel error";
         }
       }
-    }).controller();
+    });
+
+    controller = el.dialog(config).controller();
   }
 
   module("controllers/dialog_controller", {
     setup: function() {
       reset();
-      initController();
     },
 
     teardown: function() {
@@ -73,22 +74,21 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   });
 
   test("initialization with channel error", function() {
-    controller.destroy();
-    reset();
     channelError = true;
-
     initController();
 
     ok($("#error .contents").text().length, "contents have been written");
   });
 
   test("doOffline", function() {
+    initController();
     controller.doOffline();
     ok($("#error .contents").text().length, "contents have been written");
     ok($("#error #offline").text().length, "offline error message has been written");
   });
 
   test("doXHRError while online, no network info given", function() {
+    initController();
     controller.doXHRError();
     ok($("#error .contents").text().length, "contents have been written");
     ok($("#error #action").text().length, "action contents have been written");
@@ -96,6 +96,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   });
 
   test("doXHRError while online, network info given", function() {
+    initController();
     controller.doXHRError({
       network: {
         type: "POST",
@@ -108,6 +109,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   });
 
   test("doXHRError while offline does not update contents", function() {
+    initController();
     controller.doOffline();
     $("#error #action").remove();
 
@@ -116,5 +118,39 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   });
 
 
+  /*
+  test("doCheckAuth with registered requiredEmail, authenticated", function() {
+    initController({
+      requiredEmail: "registered@testuser.com" 
+    });
+
+    controller.doCheckAuth();
+  });
+
+  test("doCheckAuth with registered requiredEmail, not authenticated", function() {
+    initController({
+      requiredEmail: "registered@testuser.com" 
+    });
+
+    controller.doCheckAuth();
+  });
+
+  test("doCheckAuth with unregistered requiredEmail, not authenticated", function() {
+    initController({
+      requiredEmail: "unregistered@testuser.com" 
+    });
+
+    controller.doCheckAuth();
+  });
+
+  test("doCheckAuth with unregistered requiredEmail, authenticated as other user", function() {
+    initController({
+      requiredEmail: "unregistered@testuser.com" 
+    });
+
+    controller.doCheckAuth();
+  });
+*/
+
 });
 
diff --git a/resources/static/test/qunit/controllers/pickemail_controller_unit_test.js b/resources/static/test/qunit/controllers/pickemail_controller_unit_test.js
index 1a88f607c5e1a41c29abca1b99414aebbc11e44a..d1b416313b86d11c4f3b9e9206925666882b673c 100644
--- a/resources/static/test/qunit/controllers/pickemail_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/pickemail_controller_unit_test.js
@@ -78,7 +78,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
     var radioButton = $("input[type=radio]").eq(1);
     ok(radioButton.is(":checked"), "the email address we specified is checked");
 
-    var label = radioButton.parent();;
+    var label = radioButton.parent();
     ok(label.hasClass("preselected"), "the label has the preselected class");
   });
 
@@ -171,5 +171,17 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
     equal(storage.site.get(testOrigin, "remember"), false, "remember saved correctly");
   });
 
+  test("addEmail with valid email", function() {
+
+  });
+
+  test("addEmail with valid email with leading/trailing whitespace", function() {
+
+  });
+
+  test("addEmail with invalid email", function() {
+
+  });
+
 });
 
diff --git a/resources/static/test/qunit/controllers/required_email_controller_unit_test.js b/resources/static/test/qunit/controllers/required_email_controller_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..67319c9a7596a66e4e9a2b4dc2810e199e4fd2cd
--- /dev/null
+++ b/resources/static/test/qunit/controllers/required_email_controller_unit_test.js
@@ -0,0 +1,282 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, 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 ***** */
+steal.then(function() {
+  "use strict";
+
+  var el,
+      controller,
+      bid = BrowserID,
+      xhr = bid.Mocks.xhr,
+      user = bid.User,
+      network = bid.Network,
+      storage = bid.Storage,
+      hub = OpenAjax.hub,
+      listeners = [];
+
+  // XXX TODO Share this code with the other tests.
+  function subscribe(message, cb) {
+    listeners.push(hub.subscribe(message, cb));
+  }
+
+  function unsubscribeAll() {
+    var registration;
+    while(registration = listeners.pop()) {
+      hub.unsubscribe(registration);
+    }
+  }
+
+  module("controllers/required_email", {
+    setup: function() {
+      el = $("body");
+      $("#error").hide();
+      network.setXHR(xhr);
+      storage.clear();
+      xhr.useResult("valid");
+      xhr.setContextInfo({
+        authenticated: false
+      });
+    },
+
+    teardown: function() {
+      if (controller) {
+        try {
+          controller.destroy();
+        } catch(e) {
+          // controller may have already been deleted.
+        }
+        controller = null;
+      }
+      network.setXHR($);
+      unsubscribeAll();
+    }
+  });
+
+  function testSignIn(email, cb) {
+    setTimeout(function() {
+      equal($("#required_email").text(), email, "email set correctly");
+      equal($("#sign_in").length, 1, "sign in button shown");
+      equal($("#verify_address").length, 0, "verify address not shows");
+      cb && cb();
+      start();
+    }, 100);
+    stop();
+  }
+
+  function testVerify(email, cb) {
+    setTimeout(function() {
+      equal($("#required_email").text(), email, "email set correctly");
+      equal($("#sign_in").length, 0, "sign in button not shown");
+      equal($("#verify_address").length, 1, "verify address shows");
+      testNoPasswordSection();
+      cb && cb();
+      start();
+    }, 100);
+    stop();
+  }
+
+  function testPasswordSection() {
+    equal($("#password_section").length, 1, "password section is there");
+  }
+
+  function testNoPasswordSection() {
+    equal($("#password_section").length, 0, "password section is not there");
+  }
+
+  test("user who is not authenticated, email is registered", function() {
+    var email = "registered@testuser.com";
+    controller = el.requiredemail({
+      email: email, 
+      authenticated: false
+    }).controller();
+
+    testSignIn(email, testPasswordSection);
+  });
+
+  test("user who is not authenticated, email not registered", function() {
+    var email = "unregistered@testuser.com";
+    controller = el.requiredemail({
+      email: email, 
+      authenticated: false
+    }).controller();
+
+    testVerify(email);
+  });
+
+  test("user who is not authenticated, XHR error", function() {
+    xhr.useResult("ajaxError");
+    var email = "registered@testuser.com";
+    controller = el.requiredemail({
+      email: email, 
+      authenticated: false
+    }).controller();
+
+    stop();
+
+    setTimeout(function() {
+      ok($("#error").is(":visible"), "Error message is visible");
+      start();
+    }, 100);
+  });
+
+  test("user who is authenticated, email belongs to user", function() {
+    xhr.setContextInfo({
+      authenticated: true 
+    });
+
+    var email = "registered@testuser.com";
+    user.syncEmailKeypair(email, function() {
+      controller = el.requiredemail({
+        email: email, 
+        authenticated: true
+      }).controller();
+    });
+
+    testSignIn(email, testNoPasswordSection);
+  });
+
+  test("user who is authenticated, email does not belong to user", function() {
+    xhr.setContextInfo({
+      authenticated: true 
+    });
+
+    var email = "registered@testuser.com";
+    user.removeEmail(email, function() {
+      controller = el.requiredemail({
+        email: email, 
+        authenticated: true
+      }).controller();
+    });
+
+    testVerify(email);
+  });
+
+  test("user who is authenticated, but email unknown", function() {
+    xhr.setContextInfo({
+      authenticated: true 
+    });
+
+    var email = "unregistered@testuser.com";
+    controller = el.requiredemail({
+      email: email, 
+      authenticated: true
+    }).controller();
+
+    testVerify(email);
+  });
+
+  
+  test("signIn of an authenticated user generates an assertion", function() {
+    xhr.setContextInfo({
+      authenticated: true 
+    });
+
+    var email = "registered@testuser.com";
+    user.syncEmailKeypair(email, function() {
+      controller = el.requiredemail({
+        email: email, 
+        authenticated: true
+      }).controller();
+
+      subscribe("assertion_generated", function(item, info) {
+        ok(info.assertion, "we have an assertion");
+        start();
+      });
+
+      controller.signIn();
+    });
+
+    stop();
+  });
+
+  test("signIn of an non-authenticated user with a good password generates an assertion", function() {
+    xhr.setContextInfo({
+      authenticated: false
+    });
+
+    var email = "registered@testuser.com";
+    controller = el.requiredemail({
+      email: email, 
+      authenticated: false
+    }).controller();
+
+    subscribe("assertion_generated", function(item, info) {
+      ok(info.assertion, "we have an assertion");
+      start();
+    });
+
+    $("#password").val("password");
+    controller.signIn();
+
+    stop();
+  });
+
+
+  test("signIn of an non-authenticated user with a bad password does not generate an assertion", function() {
+    xhr.setContextInfo({
+      authenticated: false
+    });
+
+    var email = "registered@testuser.com";
+    controller = el.requiredemail({
+      email: email, 
+      authenticated: false
+    }).controller();
+
+    var assertion;
+
+    subscribe("assertion_generated", function(item, info) {
+      ok(false, "this should not have been called");
+      assertion = info.assertion;
+    });
+
+    xhr.useResult("invalid");
+    $("#password").val("badpassword");
+    controller.signIn();
+
+    setTimeout(function() {
+      // Since we are using the mock, we know the XHR result is going to be 
+      // back in less than 1000ms.  All we have to do is check whether an 
+      // assertion was generated, if so, bad jiji.
+      equal(typeof assertion, "undefined", "assertion was never generated");
+      start();
+    }, 1000);
+
+    stop();
+  });
+
+});
+
diff --git a/resources/static/test/qunit/qunit.js b/resources/static/test/qunit/qunit.js
index 8a250f9133ea61ad7256dcce2e4a1f865b3964c6..f414ec3074096f102e3f891f7d246d75fd563a17 100644
--- a/resources/static/test/qunit/qunit.js
+++ b/resources/static/test/qunit/qunit.js
@@ -22,6 +22,9 @@ steal.plugins(
       "/shared/validation",
       "/shared/helpers",
 
+      "/dialog/controllers/page_controller",
+      "/dialog/controllers/required_email_controller",
+
       "pages/browserid_unit_test",
       "pages/page_helpers_unit_test",
 
@@ -44,9 +47,13 @@ steal.plugins(
       "shared/network_unit_test",
       "shared/user_unit_test",
       "resources/channel_unit_test",
+
       "controllers/page_controller_unit_test",
       "controllers/pickemail_controller_unit_test",
       "controllers/dialog_controller_unit_test",
       "controllers/checkregistration_controller_unit_test",
-      "controllers/authenticate_controller_unit_test");
+      "controllers/authenticate_controller_unit_test",
+      "controllers/required_email_controller_unit_test"
+      
+      );