diff --git a/src/applications/auth/constants/PhabricatorCookies.php b/src/applications/auth/constants/PhabricatorCookies.php
--- a/src/applications/auth/constants/PhabricatorCookies.php
+++ b/src/applications/auth/constants/PhabricatorCookies.php
@@ -57,6 +57,12 @@
   const COOKIE_HISEC          = 'jump_to_hisec';
 
 
+  /**
+   * Stores an invite code.
+   */
+  const COOKIE_INVITE = 'invite';
+
+
 /* -(  Client ID Cookie  )--------------------------------------------------- */
 
 
diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php
--- a/src/applications/auth/controller/PhabricatorAuthController.php
+++ b/src/applications/auth/controller/PhabricatorAuthController.php
@@ -108,6 +108,9 @@
 
     // Clear the client ID / OAuth state key.
     $request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID);
+
+    // Clear the invite cookie.
+    $request->clearCookie(PhabricatorCookies::COOKIE_INVITE);
   }
 
   private function buildLoginValidateResponse(PhabricatorUser $user) {
@@ -246,4 +249,57 @@
     return array($account, $provider, null);
   }
 
+  protected function loadInvite() {
+    $invite_cookie = PhabricatorCookies::COOKIE_INVITE;
+    $invite_code = $this->getRequest()->getCookie($invite_cookie);
+    if (!$invite_code) {
+      return null;
+    }
+
+    $engine = id(new PhabricatorAuthInviteEngine())
+      ->setViewer($this->getViewer())
+      ->setUserHasConfirmedVerify(true);
+
+    try {
+      return $engine->processInviteCode($invite_code);
+    } catch (Exception $ex) {
+      // If this fails for any reason, just drop the invite. In normal
+      // circumstances, we gave them a detailed explanation of any error
+      // before they jumped into this workflow.
+      return null;
+    }
+  }
+
+  protected function renderInviteHeader(PhabricatorAuthInvite $invite) {
+    $viewer = $this->getViewer();
+
+    $invite_author = id(new PhabricatorPeopleQuery())
+      ->setViewer($viewer)
+      ->withPHIDs(array($invite->getAuthorPHID()))
+      ->needProfileImage(true)
+      ->executeOne();
+
+    // If we can't load the author for some reason, just drop this message.
+    // We lose the value of contextualizing things without author details.
+    if (!$invite_author) {
+      return null;
+    }
+
+    $invite_item = id(new PHUIObjectItemView())
+      ->setHeader(pht('Welcome to Phabricator!'))
+      ->setImageURI($invite_author->getProfileImageURI())
+      ->addAttribute(
+        pht(
+          '%s has invited you to join Phabricator.',
+          $invite_author->getFullName()));
+
+    $invite_list = id(new PHUIObjectItemListView())
+      ->addItem($invite_item)
+      ->setFlush(true);
+
+    return id(new PHUIBoxView())
+      ->addMargin(PHUI::MARGIN_LARGE)
+      ->appendChild($invite_list);
+  }
+
 }
diff --git a/src/applications/auth/controller/PhabricatorAuthInviteController.php b/src/applications/auth/controller/PhabricatorAuthInviteController.php
--- a/src/applications/auth/controller/PhabricatorAuthInviteController.php
+++ b/src/applications/auth/controller/PhabricatorAuthInviteController.php
@@ -17,8 +17,10 @@
       $engine->setUserHasConfirmedVerify(true);
     }
 
+    $invite_code = $request->getURIData('code');
+
     try {
-      $invite = $engine->processInviteCode($request->getURIData('code'));
+      $invite = $engine->processInviteCode($invite_code);
     } catch (PhabricatorAuthInviteDialogException $ex) {
       $response = $this->newDialog()
         ->setTitle($ex->getTitle())
@@ -48,10 +50,13 @@
       return id(new AphrontRedirectResponse())->setURI('/');
     }
 
+    // Give the user a cookie with the invite code and send them through
+    // normal registration. We'll adjust the flow there.
+    $request->setCookie(
+      PhabricatorCookies::COOKIE_INVITE,
+      $invite_code);
 
-    // TODO: This invite is good, but we need to drive the user through
-    // registration.
-    throw new Exception(pht('TODO: Build invite/registration workflow.'));
+    return id(new AphrontRedirectResponse())->setURI('/auth/start/');
   }
 
 
diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
--- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php
+++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
@@ -38,19 +38,25 @@
       return $response;
     }
 
-    if (!$provider->shouldAllowRegistration()) {
+    $invite = $this->loadInvite();
 
-      // TODO: This is a routine error if you click "Login" on an external
-      // auth source which doesn't allow registration. The error should be
-      // more tailored.
+    if (!$provider->shouldAllowRegistration()) {
+      if ($invite) {
+        // If the user has an invite, we allow them to register with any
+        // provider, even a login-only provider.
+      } else {
+        // TODO: This is a routine error if you click "Login" on an external
+        // auth source which doesn't allow registration. The error should be
+        // more tailored.
 
-      return $this->renderError(
-        pht(
-          'The account you are attempting to register with uses an '.
-          'authentication provider ("%s") which does not allow registration. '.
-          'An administrator may have recently disabled registration with this '.
-          'provider.',
-          $provider->getProviderName()));
+        return $this->renderError(
+          pht(
+            'The account you are attempting to register with uses an '.
+            'authentication provider ("%s") which does not allow '.
+            'registration. An administrator may have recently disabled '.
+            'registration with this provider.',
+            $provider->getProviderName()));
+      }
     }
 
     $user = new PhabricatorUser();
@@ -59,9 +65,15 @@
     $default_realname = $account->getRealName();
 
     $default_email = $account->getEmail();
+
+    if ($invite) {
+      $default_email = $invite->getEmailAddress();
+    }
+
     if (!PhabricatorUserEmail::isValidAddress($default_email)) {
       $default_email = null;
     }
+
     if ($default_email !== null) {
       // We should bypass policy here becase e.g. limiting an application use
       // to a subset of users should not allow the others to overwrite
@@ -105,7 +117,13 @@
           'address = %s',
           $default_email);
         if ($same_email) {
-          $default_email = null;
+          if ($invite) {
+            // We're allowing this to continue. The fact that we loaded the
+            // invite means that the address is nonprimary and unverified and
+            // we're OK to steal it.
+          } else {
+            $default_email = null;
+          }
         }
       }
     }
@@ -166,7 +184,13 @@
     $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
     $min_len = (int)$min_len;
 
-    if ($request->isFormPost() || !$can_edit_anything) {
+    $from_invite = $request->getStr('invite');
+    if ($from_invite && $can_edit_username) {
+      $value_username = $request->getStr('username');
+      $e_username = null;
+    }
+
+    if (($request->isFormPost() || !$can_edit_anything) && !$from_invite) {
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
       if ($must_set_password) {
@@ -252,28 +276,48 @@
         }
 
         try {
+          $verify_email = false;
+
           if ($force_verify) {
             $verify_email = true;
-          } else {
-            $verify_email =
-              ($account->getEmailVerified()) &&
-              ($value_email === $default_email);
           }
 
-          if ($provider->shouldTrustEmails() &&
-              $value_email === $default_email) {
-            $verify_email = true;
+          if ($value_email === $default_email) {
+            if ($account->getEmailVerified()) {
+              $verify_email = true;
+            }
+
+            if ($provider->shouldTrustEmails()) {
+              $verify_email = true;
+            }
+
+            if ($invite) {
+              $verify_email = true;
+            }
           }
 
-          $email_obj = id(new PhabricatorUserEmail())
-            ->setAddress($value_email)
-            ->setIsVerified((int)$verify_email);
+          $email_obj = null;
+          if ($invite) {
+            // If we have a valid invite, this email may exist but be
+            // nonprimary and unverified, so we'll reassign it.
+            $email_obj = id(new PhabricatorUserEmail())->loadOneWhere(
+              'address = %s',
+              $value_email);
+          }
+          if (!$email_obj) {
+            $email_obj = id(new PhabricatorUserEmail())
+              ->setAddress($value_email);
+          }
+
+          $email_obj->setIsVerified((int)$verify_email);
 
           $user->setUsername($value_username);
           $user->setRealname($value_realname);
 
           if ($is_setup) {
             $must_approve = false;
+          } else if ($invite) {
+            $must_approve = false;
           } else {
             $must_approve = PhabricatorEnv::getEnvConfig(
               'auth.require-approval');
@@ -285,12 +329,18 @@
             $user->setIsApproved(1);
           }
 
+          if ($invite) {
+            $allow_reassign_email = true;
+          } else {
+            $allow_reassign_email = false;
+          }
+
           $user->openTransaction();
 
             $editor = id(new PhabricatorUserEditor())
               ->setActor($user);
 
-            $editor->createNewUser($user, $email_obj);
+            $editor->createNewUser($user, $email_obj, $allow_reassign_email);
             if ($must_set_password) {
               $envelope = new PhutilOpaqueEnvelope($value_password);
               $editor->changePassword($user, $envelope);
@@ -314,6 +364,10 @@
             $this->sendWaitingForApprovalEmail($user);
           }
 
+          if ($invite) {
+            $invite->setAcceptedByPHID($user->getPHID())->save();
+          }
+
           return $this->loginUser($user);
         } catch (AphrontDuplicateKeyQueryException $exception) {
           $same_username = id(new PhabricatorUser())->loadOneWhere(
@@ -374,21 +428,30 @@
           ->setError($e_username));
     }
 
+    if ($can_edit_realname) {
+      $form->appendChild(
+        id(new AphrontFormTextControl())
+          ->setLabel(pht('Real Name'))
+          ->setName('realName')
+          ->setValue($value_realname)
+          ->setError($e_realname));
+    }
+
     if ($must_set_password) {
       $form->appendChild(
         id(new AphrontFormPasswordControl())
           ->setLabel(pht('Password'))
           ->setName('password')
+          ->setError($e_password));
+      $form->appendChild(
+        id(new AphrontFormPasswordControl())
+          ->setLabel(pht('Confirm Password'))
+          ->setName('confirm')
           ->setError($e_password)
           ->setCaption(
             $min_len
               ? pht('Minimum length of %d characters.', $min_len)
               : null));
-      $form->appendChild(
-        id(new AphrontFormPasswordControl())
-          ->setLabel(pht('Confirm Password'))
-          ->setName('confirm')
-          ->setError($e_password));
     }
 
     if ($can_edit_email) {
@@ -401,15 +464,6 @@
           ->setError($e_email));
     }
 
-    if ($can_edit_realname) {
-      $form->appendChild(
-        id(new AphrontFormTextControl())
-          ->setLabel(pht('Real Name'))
-          ->setName('realName')
-          ->setValue($value_realname)
-          ->setError($e_realname));
-    }
-
     if ($must_set_password) {
       $form->appendChild(
         id(new AphrontFormRecaptchaControl())
@@ -459,10 +513,16 @@
       ->setForm($form)
       ->setFormErrors($errors);
 
+    $invite_header = null;
+    if ($invite) {
+      $invite_header = $this->renderInviteHeader($invite);
+    }
+
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $welcome_view,
+        $invite_header,
         $object_box,
       ),
       array(
diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php
--- a/src/applications/auth/controller/PhabricatorAuthStartController.php
+++ b/src/applications/auth/controller/PhabricatorAuthStartController.php
@@ -109,14 +109,21 @@
       }
     }
 
+    $invite = $this->loadInvite();
+
     $not_buttons = array();
     $are_buttons = array();
     $providers = msort($providers, 'getLoginOrder');
     foreach ($providers as $provider) {
+      if ($invite) {
+        $form = $provider->buildInviteForm($this);
+      } else {
+        $form = $provider->buildLoginForm($this);
+      }
       if ($provider->isLoginFormAButton()) {
-        $are_buttons[] = $provider->buildLoginForm($this);
+        $are_buttons[] = $form;
       } else {
-        $not_buttons[] = $provider->buildLoginForm($this);
+        $not_buttons[] = $form;
       }
     }
 
@@ -159,6 +166,11 @@
     $login_message = PhabricatorEnv::getEnvConfig('auth.login-message');
     $login_message = phutil_safe_html($login_message);
 
+    $invite_message = null;
+    if ($invite) {
+      $invite_message = $this->renderInviteHeader($invite);
+    }
+
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb(pht('Login'));
     $crumbs->setBorder(true);
@@ -167,6 +179,7 @@
       array(
         $crumbs,
         $login_message,
+        $invite_message,
         $out,
       ),
       array(
diff --git a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php
--- a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php
+++ b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php
@@ -250,7 +250,7 @@
   }
 
   private function getLogoutURI() {
-    return '/auth/logout/';
+    return '/logout/';
   }
 
 }
diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php
--- a/src/applications/auth/provider/PhabricatorAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorAuthProvider.php
@@ -158,6 +158,10 @@
     return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
   }
 
+  public function buildInviteForm(PhabricatorAuthStartController $controller) {
+    return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
+  }
+
   abstract public function processLoginRequest(
     PhabricatorAuthLoginController $controller);
 
@@ -401,6 +405,8 @@
       $button_text = pht('Link External Account');
     } else if ($mode == 'refresh') {
       $button_text = pht('Refresh Account Link');
+    } else if ($mode == 'invite') {
+      $button_text = pht('Register Account');
     } else if ($this->shouldAllowRegistration()) {
       $button_text = pht('Login or Register');
     } else {
diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
--- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
@@ -135,6 +135,29 @@
     return $this->renderPasswordLoginForm($request);
   }
 
+  public function buildInviteForm(
+    PhabricatorAuthStartController $controller) {
+    $request = $controller->getRequest();
+    $viewer = $request->getViewer();
+
+    $form = id(new AphrontFormView())
+      ->setUser($viewer)
+      ->addHiddenInput('invite', true)
+      ->appendChild(
+        id(new AphrontFormTextControl())
+          ->setLabel(pht('Username'))
+          ->setName('username'));
+
+    $dialog = id(new AphrontDialogView())
+      ->setUser($viewer)
+      ->setTitle(pht('Register an Account'))
+      ->appendForm($form)
+      ->setSubmitURI('/auth/register/')
+      ->addSubmitButton(pht('Continue'));
+
+    return $dialog;
+  }
+
   public function buildLinkForm(
     PhabricatorAuthLinkController $controller) {
     throw new Exception("Password providers can't be linked.");
diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php
--- a/src/applications/people/editor/PhabricatorUserEditor.php
+++ b/src/applications/people/editor/PhabricatorUserEditor.php
@@ -23,14 +23,25 @@
    */
   public function createNewUser(
     PhabricatorUser $user,
-    PhabricatorUserEmail $email) {
+    PhabricatorUserEmail $email,
+    $allow_reassign = false) {
 
     if ($user->getID()) {
       throw new Exception('User has already been created!');
     }
 
+    $is_reassign = false;
     if ($email->getID()) {
-      throw new Exception('Email has already been created!');
+      if ($allow_reassign) {
+        if ($email->getIsPrimary()) {
+          throw new Exception(
+            pht(
+              'Primary email addresses can not be reassigned.'));
+        }
+        $is_reassign = true;
+      } else {
+        throw new Exception('Email has already been created!');
+      }
     }
 
     if (!PhabricatorUser::validateUsername($user->getUsername())) {
@@ -71,6 +82,15 @@
       $log->setNewValue($email->getAddress());
       $log->save();
 
+      if ($is_reassign) {
+        $log = PhabricatorUserLog::initializeNewLog(
+          $this->requireActor(),
+          $user->getPHID(),
+          PhabricatorUserLog::ACTION_EMAIL_REASSIGN);
+        $log->setNewValue($email->getAddress());
+        $log->save();
+      }
+
     $user->saveTransaction();
 
     return $this;