diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php
index 99e051a1e..8c4b66c3e 100644
--- a/src/aphront/AphrontRequest.php
+++ b/src/aphront/AphrontRequest.php
@@ -1,543 +1,597 @@
 <?php
 
 /**
  * @task data   Accessing Request Data
  * @task cookie Managing Cookies
  *
  */
 final class AphrontRequest {
 
   // NOTE: These magic request-type parameters are automatically included in
   // certain requests (e.g., by phabricator_form(), JX.Request,
   // JX.Workflow, and ConduitClient) and help us figure out what sort of
   // response the client expects.
 
   const TYPE_AJAX = '__ajax__';
   const TYPE_FORM = '__form__';
   const TYPE_CONDUIT = '__conduit__';
   const TYPE_WORKFLOW = '__wflow__';
   const TYPE_CONTINUE = '__continue__';
   const TYPE_PREVIEW = '__preview__';
 
   private $host;
   private $path;
   private $requestData;
   private $user;
   private $applicationConfiguration;
 
   final public function __construct($host, $path) {
     $this->host = $host;
     $this->path = $path;
   }
 
   final public function setApplicationConfiguration(
     $application_configuration) {
     $this->applicationConfiguration = $application_configuration;
     return $this;
   }
 
   final public function getApplicationConfiguration() {
     return $this->applicationConfiguration;
   }
 
   final public function setPath($path) {
     $this->path = $path;
     return $this;
   }
 
   final public function getPath() {
     return $this->path;
   }
 
   final public function getHost() {
     // The "Host" header may include a port number, or may be a malicious
     // header in the form "realdomain.com:ignored@evil.com". Invoke the full
     // parser to extract the real domain correctly. See here for coverage of
     // a similar issue in Django:
     //
     //  https://www.djangoproject.com/weblog/2012/oct/17/security/
     $uri = new PhutilURI('http://'.$this->host);
     return $uri->getDomain();
   }
 
 
 /* -(  Accessing Request Data  )--------------------------------------------- */
 
 
   /**
    * @task data
    */
   final public function setRequestData(array $request_data) {
     $this->requestData = $request_data;
     return $this;
   }
 
 
   /**
    * @task data
    */
   final public function getRequestData() {
     return $this->requestData;
   }
 
 
   /**
    * @task data
    */
   final public function getInt($name, $default = null) {
     if (isset($this->requestData[$name])) {
       return (int)$this->requestData[$name];
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   final public function getBool($name, $default = null) {
     if (isset($this->requestData[$name])) {
       if ($this->requestData[$name] === 'true') {
         return true;
       } else if ($this->requestData[$name] === 'false') {
         return false;
       } else {
         return (bool)$this->requestData[$name];
       }
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   final public function getStr($name, $default = null) {
     if (isset($this->requestData[$name])) {
       $str = (string)$this->requestData[$name];
       // Normalize newline craziness.
       $str = str_replace(
         array("\r\n", "\r"),
         array("\n", "\n"),
         $str);
       return $str;
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   final public function getArr($name, $default = array()) {
     if (isset($this->requestData[$name]) &&
         is_array($this->requestData[$name])) {
       return $this->requestData[$name];
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   final public function getStrList($name, $default = array()) {
     if (!isset($this->requestData[$name])) {
       return $default;
     }
     $list = $this->getStr($name);
     $list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
     return $list;
   }
 
 
   /**
    * @task data
    */
   final public function getExists($name) {
     return array_key_exists($name, $this->requestData);
   }
 
   final public function getFileExists($name) {
     return isset($_FILES[$name]) &&
            (idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
   }
 
   final public function isHTTPGet() {
     return ($_SERVER['REQUEST_METHOD'] == 'GET');
   }
 
   final public function isHTTPPost() {
     return ($_SERVER['REQUEST_METHOD'] == 'POST');
   }
 
   final public function isAjax() {
     return $this->getExists(self::TYPE_AJAX);
   }
 
   final public function isJavelinWorkflow() {
     return $this->getExists(self::TYPE_WORKFLOW);
   }
 
   final public function isConduit() {
     return $this->getExists(self::TYPE_CONDUIT);
   }
 
   public static function getCSRFTokenName() {
     return '__csrf__';
   }
 
   public static function getCSRFHeaderName() {
     return 'X-Phabricator-Csrf';
   }
 
   final public function validateCSRF() {
     $token_name = self::getCSRFTokenName();
     $token = $this->getStr($token_name);
 
     // No token in the request, check the HTTP header which is added for Ajax
     // requests.
     if (empty($token)) {
       $token = self::getHTTPHeader(self::getCSRFHeaderName());
     }
 
     $valid = $this->getUser()->validateCSRFToken($token);
     if (!$valid) {
 
       // Add some diagnostic details so we can figure out if some CSRF issues
       // are JS problems or people accessing Ajax URIs directly with their
       // browsers.
       $more_info = array();
 
       if ($this->isAjax()) {
         $more_info[] = pht('This was an Ajax request.');
       } else {
         $more_info[] = pht('This was a Web request.');
       }
 
       if ($token) {
         $more_info[] = pht('This request had an invalid CSRF token.');
       } else {
         $more_info[] = pht('This request had no CSRF token.');
       }
 
       // Give a more detailed explanation of how to avoid the exception
       // in developer mode.
       if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
         // TODO: Clean this up, see T1921.
         $more_info[] =
           "To avoid this error, use phabricator_form() to construct forms. " .
           "If you are already using phabricator_form(), make sure the form " .
           "'action' uses a relative URI (i.e., begins with a '/'). Forms " .
           "using absolute URIs do not include CSRF tokens, to prevent " .
           "leaking tokens to external sites.\n\n" .
           "If this page performs writes which do not require CSRF " .
           "protection (usually, filling caches or logging), you can use " .
           "AphrontWriteGuard::beginScopedUnguardedWrites() to temporarily " .
           "bypass CSRF protection while writing. You should use this only " .
           "for writes which can not be protected with normal CSRF " .
           "mechanisms.\n\n" .
           "Some UI elements (like PhabricatorActionListView) also have " .
           "methods which will allow you to render links as forms (like " .
           "setRenderAsForm(true)).";
       }
 
       // This should only be able to happen if you load a form, pull your
       // internet for 6 hours, and then reconnect and immediately submit,
       // but give the user some indication of what happened since the workflow
       // is incredibly confusing otherwise.
       throw new AphrontCSRFException(
         pht(
           "You are trying to save some data to Phabricator, but the request ".
           "your browser made included an incorrect token. Reload the page ".
           "and try again. You may need to clear your cookies.\n\n%s",
           implode("\n", $more_info)));
     }
 
     return true;
   }
 
   final public function isFormPost() {
     $post = $this->getExists(self::TYPE_FORM) &&
             $this->isHTTPPost();
 
     if (!$post) {
       return false;
     }
 
     return $this->validateCSRF();
   }
 
   final public function setCookiePrefix($prefix) {
     $this->cookiePrefix = $prefix;
     return $this;
   }
 
   final private function getPrefixedCookieName($name) {
     if (strlen($this->cookiePrefix)) {
       return $this->cookiePrefix.'_'.$name;
     } else {
       return $name;
     }
   }
 
   final public function getCookie($name, $default = null) {
     $name = $this->getPrefixedCookieName($name);
-    return idx($_COOKIE, $name, $default);
+    $value = idx($_COOKIE, $name, $default);
+
+    // Internally, PHP deletes cookies by setting them to the value 'deleted'
+    // with an expiration date in the past.
+
+    // At least in Safari, the browser may send this cookie anyway in some
+    // circumstances. After logging out, the 302'd GET to /login/ consistently
+    // includes deleted cookies on my local install. If a cookie value is
+    // literally 'deleted', pretend it does not exist.
+
+    if ($value === 'deleted') {
+      return null;
+    }
+
+    return $value;
   }
 
   final public function clearCookie($name) {
     $name = $this->getPrefixedCookieName($name);
-    $this->setCookie($name, '', time() - (60 * 60 * 24 * 30));
+    $this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
     unset($_COOKIE[$name]);
   }
 
   /**
    * Get the domain which cookies should be set on for this request, or null
    * if the request does not correspond to a valid cookie domain.
    *
    * @return PhutilURI|null   Domain URI, or null if no valid domain exists.
    *
    * @task cookie
    */
   private function getCookieDomainURI() {
     if (PhabricatorEnv::getEnvConfig('security.require-https') &&
         !$this->isHTTPS()) {
       return null;
     }
 
     $host = $this->getHost();
 
     // If there's no base domain configured, just use whatever the request
     // domain is. This makes setup easier, and we'll tell administrators to
     // configure a base domain during the setup process.
     $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
     if (!strlen($base_uri)) {
       return new PhutilURI('http://'.$host.'/');
     }
 
     $alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
     $allowed_uris = array_merge(
       array($base_uri),
       $alternates);
 
     foreach ($allowed_uris as $allowed_uri) {
       $uri = new PhutilURI($allowed_uri);
       if ($uri->getDomain() == $host) {
         return $uri;
       }
     }
 
     return null;
   }
 
   /**
    * Determine if security policy rules will allow cookies to be set when
    * responding to the request.
    *
    * @return bool True if setCookie() will succeed. If this method returns
    *              false, setCookie() will throw.
    *
    * @task cookie
    */
   final public function canSetCookies() {
     return (bool)$this->getCookieDomainURI();
   }
 
-  final public function setCookie($name, $value, $expire = null) {
+
+  /**
+   * Set a cookie which does not expire for a long time.
+   *
+   * To set a temporary cookie, see @{method:setTemporaryCookie}.
+   *
+   * @param string  Cookie name.
+   * @param string  Cookie value.
+   * @return this
+   * @task cookie
+   */
+  final public function setCookie($name, $value) {
+    $far_future = time() + (60 * 60 * 24 * 365 * 5);
+    return $this->setCookieWithExpiration($name, $value, $far_future);
+  }
+
+
+  /**
+   * Set a cookie which expires soon.
+   *
+   * To set a durable cookie, see @{method:setCookie}.
+   *
+   * @param string  Cookie name.
+   * @param string  Cookie value.
+   * @return this
+   * @task cookie
+   */
+  final public function setTemporaryCookie($name, $value) {
+    return $this->setCookieWithExpiration($name, $value, 0);
+  }
+
+
+  /**
+   * Set a cookie with a given expiration policy.
+   *
+   * @param string  Cookie name.
+   * @param string  Cookie value.
+   * @param int     Epoch timestamp for cookie expiration.
+   * @return this
+   * @task cookie
+   */
+  final private function setCookieWithExpiration(
+    $name,
+    $value,
+    $expire) {
 
     $is_secure = false;
 
     $base_domain_uri = $this->getCookieDomainURI();
     if (!$base_domain_uri) {
       $configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
       $accessed_as = $this->getHost();
 
       throw new Exception(
         pht(
           'This Phabricator install is configured as "%s", but you are '.
           'using the domain name "%s" to access a page which is trying to '.
           'set a cookie. Acccess Phabricator on the configured primary '.
           'domain or a configured alternate domain. Phabricator will not '.
           'set cookies on other domains for security reasons.',
           $configured_as,
           $accessed_as));
     }
 
     $base_domain = $base_domain_uri->getDomain();
     $is_secure = ($base_domain_uri->getProtocol() == 'https');
 
-    if ($expire === null) {
-      $expire = time() + (60 * 60 * 24 * 365 * 5);
-    }
-
     $name = $this->getPrefixedCookieName($name);
 
     if (php_sapi_name() == 'cli') {
       // Do nothing, to avoid triggering "Cannot modify header information"
       // warnings.
 
       // TODO: This is effectively a test for whether we're running in a unit
       // test or not. Move this actual call to HTTPSink?
     } else {
       setcookie(
         $name,
         $value,
         $expire,
         $path = '/',
         $base_domain,
         $is_secure,
         $http_only = true);
     }
 
     $_COOKIE[$name] = $value;
 
     return $this;
   }
 
   final public function setUser($user) {
     $this->user = $user;
     return $this;
   }
 
   final public function getUser() {
     return $this->user;
   }
 
   final public function getRequestURI() {
     $get = $_GET;
     unset($get['__path__']);
     $path = phutil_escape_uri($this->getPath());
     return id(new PhutilURI($path))->setQueryParams($get);
   }
 
   final public function isDialogFormPost() {
     return $this->isFormPost() && $this->getStr('__dialog__');
   }
 
   final public function getRemoteAddr() {
     return $_SERVER['REMOTE_ADDR'];
   }
 
   public function isHTTPS() {
     if (empty($_SERVER['HTTPS'])) {
       return false;
     }
     if (!strcasecmp($_SERVER["HTTPS"], "off")) {
       return false;
     }
     return true;
   }
 
   public function isContinueRequest() {
     return $this->isFormPost() && $this->getStr('__continue__');
   }
 
   public function isPreviewRequest() {
     return $this->isFormPost() && $this->getStr('__preview__');
   }
 
   /**
    * Get application request parameters in a flattened form suitable for
    * inclusion in an HTTP request, excluding parameters with special meanings.
    * This is primarily useful if you want to ask the user for more input and
    * then resubmit their request.
    *
    * @return  dict<string, string>  Original request parameters.
    */
   public function getPassthroughRequestParameters() {
     return self::flattenData($this->getPassthroughRequestData());
   }
 
   /**
    * Get request data other than "magic" parameters.
    *
    * @return dict<string, wild> Request data, with magic filtered out.
    */
   public function getPassthroughRequestData() {
     $data = $this->getRequestData();
 
     // Remove magic parameters like __dialog__ and __ajax__.
     foreach ($data as $key => $value) {
       if (!strncmp($key, '__', 2)) {
         unset($data[$key]);
       }
     }
 
     return $data;
   }
 
 
   /**
    * Flatten an array of key-value pairs (possibly including arrays as values)
    * into a list of key-value pairs suitable for submitting via HTTP request
    * (with arrays flattened).
    *
    * @param   dict<string, wild>    Data to flatten.
    * @return  dict<string, string>  Flat data suitable for inclusion in an HTTP
    *                                request.
    */
   public static function flattenData(array $data) {
     $result = array();
     foreach ($data as $key => $value) {
       if (is_array($value)) {
         foreach (self::flattenData($value) as $fkey => $fvalue) {
           $fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
           $result[$key.$fkey] = $fvalue;
         }
       } else {
         $result[$key] = (string)$value;
       }
     }
 
     ksort($result);
 
     return $result;
   }
 
 
   /**
    * Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
    *
    * This function accepts a canonical header name, like `"Accept-Encoding"`,
    * and looks up the appropriate value in `$_SERVER` (in this case,
    * `"HTTP_ACCEPT_ENCODING"`).
    *
    * @param   string        Canonical header name, like `"Accept-Encoding"`.
    * @param   wild          Default value to return if header is not present.
    * @param   array?        Read this instead of `$_SERVER`.
    * @return  string|wild   Header value if present, or `$default` if not.
    */
   public static function getHTTPHeader($name, $default = null, $data = null) {
     // PHP mangles HTTP headers by uppercasing them and replacing hyphens with
     // underscores, then prepending 'HTTP_'.
     $php_index = strtoupper($name);
     $php_index = str_replace('-', '_', $php_index);
 
     $try_names = array();
 
     $try_names[] = 'HTTP_'.$php_index;
     if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
       // These headers may be available under alternate names. See
       // http://www.php.net/manual/en/reserved.variables.server.php#110763
       $try_names[] = $php_index;
     }
 
     if ($data === null) {
       $data = $_SERVER;
     }
 
     foreach ($try_names as $try_name) {
       if (array_key_exists($try_name, $data)) {
         return $data[$try_name];
       }
     }
 
     return $default;
   }
 
 }
diff --git a/src/applications/auth/constants/PhabricatorCookies.php b/src/applications/auth/constants/PhabricatorCookies.php
index 0bb34569f..ffba1b86e 100644
--- a/src/applications/auth/constants/PhabricatorCookies.php
+++ b/src/applications/auth/constants/PhabricatorCookies.php
@@ -1,130 +1,158 @@
 <?php
 
 /**
  * Consolidates Phabricator application cookies, including registration
  * and session management.
  *
- * @task next Next URI Cookie
+ * @task clientid   Client ID Cookie
+ * @task next       Next URI Cookie
  */
 final class PhabricatorCookies extends Phobject {
 
   /**
    * Stores the login username for password authentication. This is just a
    * display value for convenience, used to prefill the login form. It is not
    * authoritative.
    */
   const COOKIE_USERNAME       = 'phusr';
 
 
   /**
    * Stores the user's current session ID. This is authoritative and establishes
    * the user's identity.
    */
   const COOKIE_SESSION        = 'phsid';
 
 
   /**
    * Stores a secret used during new account registration to prevent an attacker
    * from tricking a victim into registering an account which is linked to
    * credentials the attacker controls.
    */
   const COOKIE_REGISTRATION   = 'phreg';
 
 
   /**
    * Stores a secret used during OAuth2 handshakes to prevent various attacks
    * where an attacker hands a victim a URI corresponding to the middle of an
    * OAuth2 workflow and we might otherwise do something sketchy. Particularly,
    * this corresponds to the OAuth2 "code".
    */
   const COOKIE_CLIENTID       = 'phcid';
 
 
   /**
    * Stores the URI to redirect the user to after login. This allows users to
    * visit a path like `/feed/`, be prompted to login, and then be redirected
    * back to `/feed/` after the workflow completes.
    */
   const COOKIE_NEXTURI        = 'next_uri';
 
 
+/* -(  Client ID Cookie  )--------------------------------------------------- */
+
+
+  /**
+   * Set the client ID cookie. This is a random cookie used like a CSRF value
+   * during authentication workflows.
+   *
+   * @param AphrontRequest  Request to modify.
+   * @return void
+   * @task clientid
+   */
+  public static function setClientIDCookie(AphrontRequest $request) {
+
+    // NOTE: See T3471 for some discussion. Some browsers and browser extensions
+    // can make duplicate requests, so we overwrite this cookie only if it is
+    // not present in the request. The cookie lifetime is limited by making it
+    // temporary and clearing it when users log out.
+
+    $value = $request->getCookie(self::COOKIE_CLIENTID);
+    if (!strlen($value)) {
+      $request->setTemporaryCookie(
+        self::COOKIE_CLIENTID,
+        Filesystem::readRandomCharacters(16));
+    }
+  }
+
+
 /* -(  Next URI Cookie  )---------------------------------------------------- */
 
 
   /**
    * Set the Next URI cookie. We only write the cookie if it wasn't recently
    * written, to avoid writing over a real URI with a bunch of "humans.txt"
    * stuff. See T3793 for discussion.
    *
    * @param   AphrontRequest    Request to write to.
    * @param   string            URI to write.
    * @param   bool              Write this cookie even if we have a fresh
    *                            cookie already.
    * @return  void
    *
    * @task next
    */
   public static function setNextURICookie(
     AphrontRequest $request,
     $next_uri,
     $force = false) {
 
     if (!$force) {
       $cookie_value = $request->getCookie(self::COOKIE_NEXTURI);
       list($set_at, $current_uri) = self::parseNextURICookie($cookie_value);
 
       // If the cookie was set within the last 2 minutes, don't overwrite it.
       // Primarily, this prevents browser requests for resources which do not
       // exist (like "humans.txt" and various icons) from overwriting a normal
       // URI like "/feed/".
       if ($set_at > (time() - 120)) {
         return;
       }
     }
 
     $new_value = time().','.$next_uri;
-    $request->setCookie(self::COOKIE_NEXTURI, $new_value);
+    $request->setTemporaryCookie(self::COOKIE_NEXTURI, $new_value);
   }
 
 
   /**
    * Read the URI out of the Next URI cookie.
    *
    * @param   AphrontRequest  Request to examine.
    * @return  string|null     Next URI cookie's URI value.
    *
    * @task next
    */
   public static function getNextURICookie(AphrontRequest $request) {
     $cookie_value = $request->getCookie(self::COOKIE_NEXTURI);
     list($set_at, $next_uri) = self::parseNExtURICookie($cookie_value);
 
     return $next_uri;
   }
 
 
   /**
    * Parse a Next URI cookie into its components.
    *
    * @param   string        Raw cookie value.
    * @return  list<string>  List of timestamp and URI.
    *
    * @task next
    */
   private static function parseNextURICookie($cookie) {
     // Old cookies look like: /uri
     // New cookies look like: timestamp,/uri
 
     if (!strlen($cookie)) {
       return null;
     }
 
     if (strpos($cookie, ',') !== false) {
       list($timestamp, $uri) = explode(',', $cookie, 2);
       return array((int)$timestamp, $uri);
     }
 
     return array(0, $cookie);
   }
 
 }
diff --git a/src/applications/auth/controller/PhabricatorAuthLinkController.php b/src/applications/auth/controller/PhabricatorAuthLinkController.php
index 44be8adfb..46edb6812 100644
--- a/src/applications/auth/controller/PhabricatorAuthLinkController.php
+++ b/src/applications/auth/controller/PhabricatorAuthLinkController.php
@@ -1,135 +1,133 @@
 <?php
 
 final class PhabricatorAuthLinkController
   extends PhabricatorAuthController {
 
   private $action;
   private $providerKey;
 
   public function willProcessRequest(array $data) {
     $this->providerKey = $data['pkey'];
     $this->action = $data['action'];
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $provider = PhabricatorAuthProvider::getEnabledProviderByKey(
       $this->providerKey);
     if (!$provider) {
       return new Aphront404Response();
     }
 
     switch ($this->action) {
       case 'link':
         if (!$provider->shouldAllowAccountLink()) {
           return $this->renderErrorPage(
             pht('Account Not Linkable'),
             array(
               pht('This provider is not configured to allow linking.'),
             ));
         }
         break;
       case 'refresh':
         if (!$provider->shouldAllowAccountRefresh()) {
           return $this->renderErrorPage(
             pht('Account Not Refreshable'),
             array(
               pht('This provider does not allow refreshing.'),
             ));
         }
         break;
       default:
         return new Aphront400Response();
     }
 
     $account = id(new PhabricatorExternalAccount())->loadOneWhere(
       'accountType = %s AND accountDomain = %s AND userPHID = %s',
       $provider->getProviderType(),
       $provider->getProviderDomain(),
       $viewer->getPHID());
 
     switch ($this->action) {
       case 'link':
         if ($account) {
           return $this->renderErrorPage(
             pht('Account Already Linked'),
             array(
               pht(
                 'Your Phabricator account is already linked to an external '.
                 'account for this provider.'),
             ));
         }
         break;
       case 'refresh':
         if (!$account) {
           return $this->renderErrorPage(
             pht('No Account Linked'),
             array(
               pht(
                 'You do not have a linked account on this provider, and thus '.
                 'can not refresh it.'),
             ));
         }
         break;
       default:
         return new Aphront400Response();
     }
 
     $panel_uri = '/settings/panel/external/';
 
-    $request->setCookie(
-      PhabricatorCookies::COOKIE_CLIENTID,
-      Filesystem::readRandomCharacters(16));
+    PhabricatorCookies::setClientIDCookie($request);
 
     switch ($this->action) {
       case 'link':
         $form = $provider->buildLinkForm($this);
         break;
       case 'refresh':
         $form = $provider->buildRefreshForm($this);
         break;
       default:
         return new Aphront400Response();
     }
 
     if ($provider->isLoginFormAButton()) {
       require_celerity_resource('auth-css');
       $form = phutil_tag(
         'div',
         array(
           'class' => 'phabricator-link-button pl',
         ),
         $form);
     }
 
     switch ($this->action) {
       case 'link':
         $name = pht('Link Account');
         $title = pht('Link %s Account', $provider->getProviderName());
         break;
       case 'refresh':
         $name = pht('Refresh Account');
         $title = pht('Refresh %s Account', $provider->getProviderName());
         break;
       default:
         return new Aphront400Response();
     }
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb(pht('Link Account'), $panel_uri);
     $crumbs->addTextCrumb($provider->getProviderName($name));
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $form,
       ),
       array(
         'title' => $title,
         'device' => true,
       ));
   }
 
 }
diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php
index af0708b57..804a2731d 100644
--- a/src/applications/auth/controller/PhabricatorAuthLoginController.php
+++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php
@@ -1,252 +1,252 @@
 <?php
 
 final class PhabricatorAuthLoginController
   extends PhabricatorAuthController {
 
   private $providerKey;
   private $extraURIData;
   private $provider;
 
   public function shouldRequireLogin() {
     return false;
   }
 
   public function shouldAllowRestrictedParameter($parameter_name) {
     // Whitelist the OAuth 'code' parameter.
 
     if ($parameter_name == 'code') {
       return true;
     }
     return parent::shouldAllowRestrictedParameter($parameter_name);
   }
 
   public function willProcessRequest(array $data) {
     $this->providerKey = $data['pkey'];
     $this->extraURIData = idx($data, 'extra');
   }
 
   public function getExtraURIData() {
     return $this->extraURIData;
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $response = $this->loadProvider();
     if ($response) {
       return $response;
     }
 
     $provider = $this->provider;
 
     try {
       list($account, $response) = $provider->processLoginRequest($this);
     } catch (PhutilAuthUserAbortedException $ex) {
       if ($viewer->isLoggedIn()) {
         // If a logged-in user cancels, take them back to the external accounts
         // panel.
         $next_uri = '/settings/panel/external/';
       } else {
         // If a logged-out user cancels, take them back to the auth start page.
         $next_uri = '/';
       }
 
       // User explicitly hit "Cancel".
       $dialog = id(new AphrontDialogView())
         ->setUser($viewer)
         ->setTitle(pht('Authentication Canceled'))
         ->appendChild(
           pht('You canceled authentication.'))
         ->addCancelButton($next_uri, pht('Continue'));
       return id(new AphrontDialogResponse())->setDialog($dialog);
     }
 
     if ($response) {
       return $response;
     }
 
     if (!$account) {
       throw new Exception(
         "Auth provider failed to load an account from processLoginRequest()!");
     }
 
     if ($account->getUserPHID()) {
       // The account is already attached to a Phabricator user, so this is
       // either a login or a bad account link request.
       if (!$viewer->isLoggedIn()) {
         if ($provider->shouldAllowLogin()) {
           return $this->processLoginUser($account);
         } else {
           return $this->renderError(
             pht(
               'The external account ("%s") you just authenticated with is '.
               'not configured to allow logins on this Phabricator install. '.
               'An administrator may have recently disabled it.',
               $provider->getProviderName()));
         }
       } else if ($viewer->getPHID() == $account->getUserPHID()) {
         // This is either an attempt to re-link an existing and already
         // linked account (which is silly) or a refresh of an external account
         // (e.g., an OAuth account).
         return id(new AphrontRedirectResponse())
           ->setURI('/settings/panel/external/');
       } else {
         return $this->renderError(
           pht(
             'The external account ("%s") you just used to login is alerady '.
             'associated with another Phabricator user account. Login to the '.
             'other Phabricator account and unlink the external account before '.
             'linking it to a new Phabricator account.',
             $provider->getProviderName()));
       }
     } else {
       // The account is not yet attached to a Phabricator user, so this is
       // either a registration or an account link request.
       if (!$viewer->isLoggedIn()) {
         if ($provider->shouldAllowRegistration()) {
           return $this->processRegisterUser($account);
         } else {
           return $this->renderError(
             pht(
               'The external account ("%s") you just authenticated with is '.
               'not configured to allow registration on this Phabricator '.
               'install. An administrator may have recently disabled it.',
               $provider->getProviderName()));
         }
       } else {
         if ($provider->shouldAllowAccountLink()) {
           return $this->processLinkUser($account);
         } else {
           return $this->renderError(
             pht(
               'The external account ("%s") you just authenticated with is '.
               'not configured to allow account linking on this Phabricator '.
               'install. An administrator may have recently disabled it.',
               $provider->getProviderName()));
         }
       }
     }
 
     // This should be unreachable, but fail explicitly if we get here somehow.
     return new Aphront400Response();
   }
 
   private function processLoginUser(PhabricatorExternalAccount $account) {
     $user = id(new PhabricatorUser())->loadOneWhere(
       'phid = %s',
       $account->getUserPHID());
 
     if (!$user) {
       return $this->renderError(
         pht(
           'The external account you just logged in with is not associated '.
           'with a valid Phabricator user.'));
     }
 
     return $this->loginUser($user);
   }
 
   private function processRegisterUser(PhabricatorExternalAccount $account) {
     $account_secret = $account->getAccountSecret();
     $register_uri = $this->getApplicationURI('register/'.$account_secret.'/');
     return $this->setAccountKeyAndContinue($account, $register_uri);
   }
 
   private function processLinkUser(PhabricatorExternalAccount $account) {
     $account_secret = $account->getAccountSecret();
     $confirm_uri = $this->getApplicationURI('confirmlink/'.$account_secret.'/');
     return $this->setAccountKeyAndContinue($account, $confirm_uri);
   }
 
   private function setAccountKeyAndContinue(
     PhabricatorExternalAccount $account,
     $next_uri) {
 
     if ($account->getUserPHID()) {
       throw new Exception("Account is already registered or linked.");
     }
 
     // Regenerate the registration secret key, set it on the external account,
     // set a cookie on the user's machine, and redirect them to registration.
     // See PhabricatorAuthRegisterController for discussion of the registration
     // key.
 
     $registration_key = Filesystem::readRandomCharacters(32);
     $account->setProperty(
       'registrationKey',
       PhabricatorHash::digest($registration_key));
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
       $account->save();
     unset($unguarded);
 
-    $this->getRequest()->setCookie(
+    $this->getRequest()->setTemporaryCookie(
       PhabricatorCookies::COOKIE_REGISTRATION,
       $registration_key);
 
     return id(new AphrontRedirectResponse())->setURI($next_uri);
   }
 
   private function loadProvider() {
     $provider = PhabricatorAuthProvider::getEnabledProviderByKey(
       $this->providerKey);
 
     if (!$provider) {
       return $this->renderError(
         pht(
           'The account you are attempting to login with uses a nonexistent '.
           'or disabled authentication provider (with key "%s"). An '.
           'administrator may have recently disabled this provider.',
           $this->providerKey));
     }
 
     $this->provider = $provider;
 
     return null;
   }
 
   protected function renderError($message) {
     return $this->renderErrorPage(
       pht('Login Failed'),
       array($message));
   }
 
   public function buildProviderPageResponse(
     PhabricatorAuthProvider $provider,
     $content) {
 
     $crumbs = $this->buildApplicationCrumbs();
 
     if ($this->getRequest()->getUser()->isLoggedIn()) {
       $crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI());
     } else {
       $crumbs->addTextCrumb(pht('Login'), $this->getApplicationURI('start/'));
     }
 
     $crumbs->addTextCrumb($provider->getProviderName());
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $content,
       ),
       array(
         'title' => pht('Login'),
         'device' => true,
       ));
   }
 
   public function buildProviderErrorResponse(
     PhabricatorAuthProvider $provider,
     $message) {
 
     $message = pht(
       'Authentication provider ("%s") encountered an error during login. %s',
       $provider->getProviderName(),
       $message);
 
     return $this->renderError($message);
   }
 
 }
diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php
index 3c1cb7491..bcb0fef83 100644
--- a/src/applications/auth/controller/PhabricatorAuthStartController.php
+++ b/src/applications/auth/controller/PhabricatorAuthStartController.php
@@ -1,214 +1,212 @@
 <?php
 
 final class PhabricatorAuthStartController
   extends PhabricatorAuthController {
 
   public function shouldRequireLogin() {
     return false;
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     if ($viewer->isLoggedIn()) {
       // Kick the user home if they are already logged in.
       return id(new AphrontRedirectResponse())->setURI('/');
     }
 
     if ($request->isAjax()) {
       return $this->processAjaxRequest();
     }
 
     if ($request->isConduit()) {
       return $this->processConduitRequest();
     }
 
     // If the user gets this far, they aren't logged in, so if they have a
     // user session token we can conclude that it's invalid: if it was valid,
     // they'd have been logged in above and never made it here. Try to clear
     // it and warn the user they may need to nuke their cookies.
 
     $session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
+
     if (strlen($session_token)) {
       $kind = PhabricatorAuthSessionEngine::getSessionKindFromToken(
         $session_token);
       switch ($kind) {
         case PhabricatorAuthSessionEngine::KIND_ANONYMOUS:
           // If this is an anonymous session. It's expected that they won't
           // be logged in, so we can just continue.
           break;
         default:
           // The session cookie is invalid, so clear it.
           $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
           $request->clearCookie(PhabricatorCookies::COOKIE_SESSION);
 
           return $this->renderError(
             pht(
               "Your login session is invalid. Try reloading the page and ".
               "logging in again. If that does not work, clear your browser ".
               "cookies."));
       }
     }
 
     $providers = PhabricatorAuthProvider::getAllEnabledProviders();
     foreach ($providers as $key => $provider) {
       if (!$provider->shouldAllowLogin()) {
         unset($providers[$key]);
       }
     }
 
     if (!$providers) {
       if ($this->isFirstTimeSetup()) {
         // If this is a fresh install, let the user register their admin
         // account.
         return id(new AphrontRedirectResponse())
           ->setURI($this->getApplicationURI('/register/'));
       }
 
       return $this->renderError(
         pht(
           "This Phabricator install is not configured with any enabled ".
           "authentication providers which can be used to log in. If you ".
           "have accidentally locked yourself out by disabling all providers, ".
           "you can use `phabricator/bin/auth recover <username>` to ".
           "recover access to an administrative account."));
     }
 
     $next_uri = $request->getStr('next');
     if (!$next_uri) {
       $next_uri_path = $this->getRequest()->getPath();
       if ($next_uri_path == '/auth/start/') {
         $next_uri = '/';
       } else {
         $next_uri = $this->getRequest()->getRequestURI();
       }
     }
 
     if (!$request->isFormPost()) {
       PhabricatorCookies::setNextURICookie($request, $next_uri);
-
-      $request->setCookie(
-        PhabricatorCookies::COOKIE_CLIENTID,
-        Filesystem::readRandomCharacters(16));
+      PhabricatorCookies::setClientIDCookie($request);
     }
 
     $not_buttons = array();
     $are_buttons = array();
     $providers = msort($providers, 'getLoginOrder');
     foreach ($providers as $provider) {
       if ($provider->isLoginFormAButton()) {
         $are_buttons[] = $provider->buildLoginForm($this);
       } else {
         $not_buttons[] = $provider->buildLoginForm($this);
       }
     }
 
     $out = array();
     $out[] = $not_buttons;
     if ($are_buttons) {
       require_celerity_resource('auth-css');
 
       foreach ($are_buttons as $key => $button) {
         $are_buttons[$key] = phutil_tag(
           'div',
           array(
             'class' => 'phabricator-login-button mmb',
           ),
           $button);
       }
 
       // If we only have one button, add a second pretend button so that we
       // always have two columns. This makes it easier to get the alignments
       // looking reasonable.
       if (count($are_buttons) == 1) {
         $are_buttons[] = null;
       }
 
       $button_columns = id(new AphrontMultiColumnView())
         ->setFluidLayout(true);
       $are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2));
       foreach ($are_buttons as $column) {
         $button_columns->addColumn($column);
       }
 
       $out[] = phutil_tag(
         'div',
         array(
           'class' => 'phabricator-login-buttons',
         ),
         $button_columns);
     }
 
     $login_message = PhabricatorEnv::getEnvConfig('auth.login-message');
     $login_message = phutil_safe_html($login_message);
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb(pht('Login'));
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $login_message,
         $out,
       ),
       array(
         'title' => pht('Login to Phabricator'),
         'device' => true,
       ));
   }
 
 
   private function processAjaxRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     // We end up here if the user clicks a workflow link that they need to
     // login to use. We give them a dialog saying "You need to login...".
 
     if ($request->isDialogFormPost()) {
       return id(new AphrontRedirectResponse())->setURI(
         $request->getRequestURI());
     }
 
     $dialog = new AphrontDialogView();
     $dialog->setUser($viewer);
     $dialog->setTitle(pht('Login Required'));
     $dialog->appendChild(pht('You must login to continue.'));
     $dialog->addSubmitButton(pht('Login'));
     $dialog->addCancelButton('/');
 
     return id(new AphrontDialogResponse())->setDialog($dialog);
   }
 
 
   private function processConduitRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     // A common source of errors in Conduit client configuration is getting
     // the request path wrong. The client will end up here, so make some
     // effort to give them a comprehensible error message.
 
     $request_path = $this->getRequest()->getPath();
     $conduit_path = '/api/<method>';
     $example_path = '/api/conduit.ping';
 
     $message = pht(
       'ERROR: You are making a Conduit API request to "%s", but the correct '.
       'HTTP request path to use in order to access a COnduit method is "%s" '.
       '(for example, "%s"). Check your configuration.',
       $request_path,
       $conduit_path,
       $example_path);
 
     return id(new AphrontPlainTextResponse())->setContent($message);
   }
 
   protected function renderError($message) {
     return $this->renderErrorPage(
       pht('Authentication Failure'),
       array($message));
   }
 
 }