Issue "anonymous" sessions for logged-out users
Summary: Ref T4339. Ref T4310. Currently, sessions look like `"afad85d675fda87a4fadd54"`, and are only issued for logged-in users. To support logged-out CSRF and (eventually) external user sessions, I made two small changes: - First, sessions now have a "kind", which is indicated by a prefix, like `"A/ab987asdcas7dca"`. This mostly allows us to issue session queries more efficiently: we don't have to issue a query at all for anonymous sessions, and can join the correct table for user and external sessions and save a query. Generally, this gives us more debugging information and more opportunity to recover from issues in a user-friendly way, as with the "invalid session" error in this diff. - Secondly, if you load a page and don't have a session, we give you an anonymous session. This is just a secret with no special significance. This does not implement CSRF yet, but gives us a client secret we can use to implement it. Test Plan: - Logged in. - Logged out. - Browsed around. - Logged in again. - Went through link/register. Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T4310, T4339 Differential Revision: https://secure.phabricator.com/D8043
This commit is contained in:
		| @@ -24,17 +24,32 @@ final class PhabricatorAuthStartController | |||||||
|       return $this->processConduitRequest(); |       return $this->processConduitRequest(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (strlen($request->getCookie(PhabricatorCookies::COOKIE_SESSION))) { |     // 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. |           // The session cookie is invalid, so clear it. | ||||||
|           $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME); |           $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME); | ||||||
|           $request->clearCookie(PhabricatorCookies::COOKIE_SESSION); |           $request->clearCookie(PhabricatorCookies::COOKIE_SESSION); | ||||||
|  |  | ||||||
|           return $this->renderError( |           return $this->renderError( | ||||||
|             pht( |             pht( | ||||||
|           "Your login session is invalid. Try reloading the page and logging ". |               "Your login session is invalid. Try reloading the page and ". | ||||||
|           "in again. If that does not work, clear your browser cookies.")); |               "logging in again. If that does not work, clear your browser ". | ||||||
|  |               "cookies.")); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     $providers = PhabricatorAuthProvider::getAllEnabledProviders(); |     $providers = PhabricatorAuthProvider::getAllEnabledProviders(); | ||||||
|     foreach ($providers as $key => $provider) { |     foreach ($providers as $key => $provider) { | ||||||
|   | |||||||
| @@ -2,7 +2,79 @@ | |||||||
|  |  | ||||||
| final class PhabricatorAuthSessionEngine extends Phobject { | final class PhabricatorAuthSessionEngine extends Phobject { | ||||||
|  |  | ||||||
|   public function loadUserForSession($session_type, $session_key) { |   /** | ||||||
|  |    * Session issued to normal users after they login through a standard channel. | ||||||
|  |    * Associates the client with a standard user identity. | ||||||
|  |    */ | ||||||
|  |   const KIND_USER      = 'U'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Session issued to users who login with some sort of credentials but do not | ||||||
|  |    * have full accounts. These are sometimes called "grey users". | ||||||
|  |    * | ||||||
|  |    * TODO: We do not currently issue these sessions, see T4310. | ||||||
|  |    */ | ||||||
|  |   const KIND_EXTERNAL  = 'X'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Session issued to logged-out users which has no real identity information. | ||||||
|  |    * Its purpose is to protect logged-out users from CSRF. | ||||||
|  |    */ | ||||||
|  |   const KIND_ANONYMOUS = 'A'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Session kind isn't known. | ||||||
|  |    */ | ||||||
|  |   const KIND_UNKNOWN   = '?'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get the session kind (e.g., anonymous, user, external account) from a | ||||||
|  |    * session token. Returns a `KIND_` constant. | ||||||
|  |    * | ||||||
|  |    * @param   string  Session token. | ||||||
|  |    * @return  const   Session kind constant. | ||||||
|  |    */ | ||||||
|  |   public static function getSessionKindFromToken($session_token) { | ||||||
|  |     if (strpos($session_token, '/') === false) { | ||||||
|  |       // Old-style session, these are all user sessions. | ||||||
|  |       return self::KIND_USER; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     list($kind, $key) = explode('/', $session_token, 2); | ||||||
|  |  | ||||||
|  |     switch ($kind) { | ||||||
|  |       case self::KIND_ANONYMOUS: | ||||||
|  |       case self::KIND_USER: | ||||||
|  |       case self::KIND_EXTERNAL: | ||||||
|  |         return $kind; | ||||||
|  |       default: | ||||||
|  |         return self::KIND_UNKNOWN; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public function loadUserForSession($session_type, $session_token) { | ||||||
|  |     $session_kind = self::getSessionKindFromToken($session_token); | ||||||
|  |     switch ($session_kind) { | ||||||
|  |       case self::KIND_ANONYMOUS: | ||||||
|  |         // Don't bother trying to load a user for an anonymous session, since | ||||||
|  |         // neither the session nor the user exist. | ||||||
|  |         return null; | ||||||
|  |       case self::KIND_UNKNOWN: | ||||||
|  |         // If we don't know what kind of session this is, don't go looking for | ||||||
|  |         // it. | ||||||
|  |         return null; | ||||||
|  |       case self::KIND_USER: | ||||||
|  |         break; | ||||||
|  |       case self::KIND_EXTERNAL: | ||||||
|  |         // TODO: Implement these (T4310). | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     $session_table = new PhabricatorAuthSession(); |     $session_table = new PhabricatorAuthSession(); | ||||||
|     $user_table = new PhabricatorUser(); |     $user_table = new PhabricatorUser(); | ||||||
|     $conn_r = $session_table->establishConnection('r'); |     $conn_r = $session_table->establishConnection('r'); | ||||||
| @@ -18,7 +90,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { | |||||||
|       $user_table->getTableName(), |       $user_table->getTableName(), | ||||||
|       $session_table->getTableName(), |       $session_table->getTableName(), | ||||||
|       $session_type, |       $session_type, | ||||||
|       PhabricatorHash::digest($session_key)); |       PhabricatorHash::digest($session_token)); | ||||||
|  |  | ||||||
|     if (!$info) { |     if (!$info) { | ||||||
|       return null; |       return null; | ||||||
| @@ -65,17 +137,22 @@ final class PhabricatorAuthSessionEngine extends Phobject { | |||||||
|    * |    * | ||||||
|    * @param   const     Session type constant (see |    * @param   const     Session type constant (see | ||||||
|    *                    @{class:PhabricatorAuthSession}). |    *                    @{class:PhabricatorAuthSession}). | ||||||
|    * @param   phid    Identity to establish a session for, usually a user PHID. |    * @param   phid|null Identity to establish a session for, usually a user | ||||||
|  |    *                    PHID. With `null`, generates an anonymous session. | ||||||
|    * @return  string    Newly generated session key. |    * @return  string    Newly generated session key. | ||||||
|    */ |    */ | ||||||
|   public function establishSession($session_type, $identity_phid) { |   public function establishSession($session_type, $identity_phid) { | ||||||
|     $session_table = new PhabricatorAuthSession(); |  | ||||||
|     $conn_w = $session_table->establishConnection('w'); |  | ||||||
|  |  | ||||||
|     // Consume entropy to generate a new session key, forestalling the eventual |     // Consume entropy to generate a new session key, forestalling the eventual | ||||||
|     // heat death of the universe. |     // heat death of the universe. | ||||||
|     $session_key = Filesystem::readRandomCharacters(40); |     $session_key = Filesystem::readRandomCharacters(40); | ||||||
|  |  | ||||||
|  |     if ($identity_phid === null) { | ||||||
|  |       return self::KIND_ANONYMOUS.'/'.$session_key; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $session_table = new PhabricatorAuthSession(); | ||||||
|  |     $conn_w = $session_table->establishConnection('w'); | ||||||
|  |  | ||||||
|     // This has a side effect of validating the session type. |     // This has a side effect of validating the session type. | ||||||
|     $session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); |     $session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -50,7 +50,6 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
| /* -(  PhabricatorPolicyInterface  )----------------------------------------- */ | /* -(  PhabricatorPolicyInterface  )----------------------------------------- */ | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ abstract class PhabricatorController extends AphrontController { | |||||||
|       $user = new PhabricatorUser(); |       $user = new PhabricatorUser(); | ||||||
|  |  | ||||||
|       $phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); |       $phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); | ||||||
|       if ($phsid) { |       if (strlen($phsid)) { | ||||||
|         $session_user = id(new PhabricatorAuthSessionEngine()) |         $session_user = id(new PhabricatorAuthSessionEngine()) | ||||||
|           ->loadUserForSession(PhabricatorAuthSession::TYPE_WEB, $phsid); |           ->loadUserForSession(PhabricatorAuthSession::TYPE_WEB, $phsid); | ||||||
|         if ($session_user) { |         if ($session_user) { | ||||||
|   | |||||||
| @@ -68,6 +68,7 @@ final class PhabricatorUserLog extends PhabricatorUserDAO { | |||||||
|     if (!$this->session) { |     if (!$this->session) { | ||||||
|       // TODO: This is not correct if there's a cookie prefix. This object |       // TODO: This is not correct if there's a cookie prefix. This object | ||||||
|       // should take an AphrontRequest. |       // should take an AphrontRequest. | ||||||
|  |       // TODO: Maybe record session kind, or drop this for anonymous sessions? | ||||||
|       $this->setSession(idx($_COOKIE, PhabricatorCookies::COOKIE_SESSION)); |       $this->setSession(idx($_COOKIE, PhabricatorCookies::COOKIE_SESSION)); | ||||||
|     } |     } | ||||||
|     $this->details['host'] = php_uname('n'); |     $this->details['host'] = php_uname('n'); | ||||||
|   | |||||||
| @@ -163,6 +163,17 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { | |||||||
|     Javelin::initBehavior('history-install'); |     Javelin::initBehavior('history-install'); | ||||||
|     Javelin::initBehavior('phabricator-gesture'); |     Javelin::initBehavior('phabricator-gesture'); | ||||||
|  |  | ||||||
|  |     // If the client doesn't have a session token, generate an anonymous | ||||||
|  |     // session. This is used to provide CSRF protection to logged-out users. | ||||||
|  |     $session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); | ||||||
|  |     if (!strlen($session_token)) { | ||||||
|  |       $anonymous_session = id(new PhabricatorAuthSessionEngine()) | ||||||
|  |         ->establishSession('web', null); | ||||||
|  |       $request->setCookie( | ||||||
|  |         PhabricatorCookies::COOKIE_SESSION, | ||||||
|  |         $anonymous_session); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     $current_token = null; |     $current_token = null; | ||||||
|     if ($user) { |     if ($user) { | ||||||
|       $current_token = $user->getCSRFToken(); |       $current_token = $user->getCSRFToken(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user