1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-25 23:10:57 +01:00

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:
epriestley 2014-01-23 14:03:22 -08:00
parent 0727418023
commit 69ddb0ced6
6 changed files with 123 additions and 20 deletions

View file

@ -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) {

View file

@ -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);

View file

@ -50,7 +50,6 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO
} }
} }
/* -( PhabricatorPolicyInterface )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -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) {

View file

@ -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');

View file

@ -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();