mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-25 06:50:55 +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:
parent
0727418023
commit
69ddb0ced6
6 changed files with 123 additions and 20 deletions
|
@ -24,18 +24,33 @@ 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
|
||||||
// The session cookie is invalid, so clear it.
|
// user session token we can conclude that it's invalid: if it was valid,
|
||||||
$request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
|
// they'd have been logged in above and never made it here. Try to clear
|
||||||
$request->clearCookie(PhabricatorCookies::COOKIE_SESSION);
|
// it and warn the user they may need to nuke their cookies.
|
||||||
|
|
||||||
return $this->renderError(
|
$session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
|
||||||
pht(
|
if (strlen($session_token)) {
|
||||||
"Your login session is invalid. Try reloading the page and logging ".
|
$kind = PhabricatorAuthSessionEngine::getSessionKindFromToken(
|
||||||
"in again. If that does not work, clear your browser cookies."));
|
$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();
|
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
|
||||||
foreach ($providers as $key => $provider) {
|
foreach ($providers as $key => $provider) {
|
||||||
if (!$provider->shouldAllowLogin()) {
|
if (!$provider->shouldAllowLogin()) {
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -63,19 +135,24 @@ final class PhabricatorAuthSessionEngine extends Phobject {
|
||||||
* You can configure the maximum number of concurrent sessions for various
|
* You can configure the maximum number of concurrent sessions for various
|
||||||
* session types in the Phabricator configuration.
|
* session types in the Phabricator configuration.
|
||||||
*
|
*
|
||||||
* @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
|
||||||
* @return string Newly generated session key.
|
* PHID. With `null`, generates an anonymous session.
|
||||||
|
* @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();
|
||||||
|
|
Loading…
Reference in a new issue