Changeset View
Changeset View
Standalone View
Standalone View
src/applications/auth/engine/PhabricatorAuthSessionEngine.php
<?php | <?php | ||||
/** | |||||
* @task hisec High Security Mode | |||||
*/ | |||||
final class PhabricatorAuthSessionEngine extends Phobject { | final class PhabricatorAuthSessionEngine extends Phobject { | ||||
/** | /** | ||||
* Session issued to normal users after they login through a standard channel. | * Session issued to normal users after they login through a standard channel. | ||||
* Associates the client with a standard user identity. | * Associates the client with a standard user identity. | ||||
*/ | */ | ||||
const KIND_USER = 'U'; | const KIND_USER = 'U'; | ||||
▲ Show 20 Lines • Show All 62 Lines • ▼ Show 20 Lines | switch ($session_kind) { | ||||
case self::KIND_EXTERNAL: | case self::KIND_EXTERNAL: | ||||
// TODO: Implement these (T4310). | // TODO: Implement these (T4310). | ||||
return null; | 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'); | ||||
$session_key = PhabricatorHash::digest($session_token); | |||||
// NOTE: We're being clever here because this happens on every page load, | // NOTE: We're being clever here because this happens on every page load, | ||||
// and by joining we can save a query. | // and by joining we can save a query. This might be getting too clever | ||||
// for its own good, though... | |||||
$info = queryfx_one( | $info = queryfx_one( | ||||
$conn_r, | $conn_r, | ||||
'SELECT s.sessionExpires AS _sessionExpires, s.id AS _sessionID, u.* | 'SELECT | ||||
s.id AS s_id, | |||||
s.sessionExpires AS s_sessionExpires, | |||||
s.sessionStart AS s_sessionStart, | |||||
s.highSecurityUntil AS s_highSecurityUntil, | |||||
u.* | |||||
FROM %T u JOIN %T s ON u.phid = s.userPHID | FROM %T u JOIN %T s ON u.phid = s.userPHID | ||||
AND s.type = %s AND s.sessionKey = %s', | AND s.type = %s AND s.sessionKey = %s', | ||||
$user_table->getTableName(), | $user_table->getTableName(), | ||||
$session_table->getTableName(), | $session_table->getTableName(), | ||||
$session_type, | $session_type, | ||||
PhabricatorHash::digest($session_token)); | $session_key); | ||||
if (!$info) { | if (!$info) { | ||||
return null; | return null; | ||||
} | } | ||||
$expires = $info['_sessionExpires']; | $session_dict = array( | ||||
$id = $info['_sessionID']; | 'userPHID' => $info['phid'], | ||||
unset($info['_sessionExpires']); | 'sessionKey' => $session_key, | ||||
unset($info['_sessionID']); | 'type' => $session_type, | ||||
); | |||||
foreach ($info as $key => $value) { | |||||
if (strncmp($key, 's_', 2) === 0) { | |||||
unset($info[$key]); | |||||
$session_dict[substr($key, 2)] = $value; | |||||
} | |||||
} | |||||
$session = id(new PhabricatorAuthSession())->loadFromArray($session_dict); | |||||
$ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); | $ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); | ||||
// If more than 20% of the time on this session has been used, refresh the | // If more than 20% of the time on this session has been used, refresh the | ||||
// TTL back up to the full duration. The idea here is that sessions are | // TTL back up to the full duration. The idea here is that sessions are | ||||
// good forever if used regularly, but get GC'd when they fall out of use. | // good forever if used regularly, but get GC'd when they fall out of use. | ||||
if (time() + (0.80 * $ttl) > $expires) { | if (time() + (0.80 * $ttl) > $session->getSessionExpires()) { | ||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); | $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); | ||||
$conn_w = $session_table->establishConnection('w'); | $conn_w = $session_table->establishConnection('w'); | ||||
queryfx( | queryfx( | ||||
$conn_w, | $conn_w, | ||||
'UPDATE %T SET sessionExpires = UNIX_TIMESTAMP() + %d WHERE id = %d', | 'UPDATE %T SET sessionExpires = UNIX_TIMESTAMP() + %d WHERE id = %d', | ||||
$session_table->getTableName(), | $session->getTableName(), | ||||
$ttl, | $ttl, | ||||
$id); | $session->getID()); | ||||
unset($unguarded); | unset($unguarded); | ||||
} | } | ||||
return $user_table->loadFromArray($info); | $user = $user_table->loadFromArray($info); | ||||
$user->attachSession($session); | |||||
return $user; | |||||
} | } | ||||
/** | /** | ||||
* Issue a new session key for a given identity. Phabricator supports | * Issue a new session key for a given identity. Phabricator supports | ||||
* different types of sessions (like "web" and "conduit") and each session | * different types of sessions (like "web" and "conduit") and each session | ||||
* type may have multiple concurrent sessions (this allows a user to be | * type may have multiple concurrent sessions (this allows a user to be | ||||
* logged in on multiple browsers at the same time, for instance). | * logged in on multiple browsers at the same time, for instance). | ||||
▲ Show 20 Lines • Show All 46 Lines • ▼ Show 20 Lines | $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); | ||||
)); | )); | ||||
$log->setSession($session_key); | $log->setSession($session_key); | ||||
$log->save(); | $log->save(); | ||||
unset($unguarded); | unset($unguarded); | ||||
return $session_key; | return $session_key; | ||||
} | } | ||||
/** | |||||
* Require high security, or prompt the user to enter high security. | |||||
* | |||||
* If the user's session is in high security, this method will return a | |||||
* token. Otherwise, it will throw an exception which will eventually | |||||
* be converted into a multi-factor authentication workflow. | |||||
* | |||||
* @param PhabricatorUser User whose session needs to be in high security. | |||||
* @param AphrontReqeust Current request. | |||||
* @param string URI to return the user to if they cancel. | |||||
* @return PhabricatorAuthHighSecurityToken Security token. | |||||
*/ | |||||
public function requireHighSecuritySession( | |||||
PhabricatorUser $viewer, | |||||
AphrontRequest $request, | |||||
$cancel_uri) { | |||||
if (!$viewer->hasSession()) { | |||||
throw new Exception( | |||||
pht('Requiring a high-security session from a user with no session!')); | |||||
} | |||||
$session = $viewer->getSession(); | |||||
$token = $this->issueHighSecurityToken($session); | |||||
if ($token) { | |||||
return $token; | |||||
} | |||||
if ($request->isHTTPPost()) { | |||||
$request->validateCSRF(); | |||||
if ($request->getExists(AphrontRequest::TYPE_HISEC)) { | |||||
// TODO: Actually verify that the user provided some multi-factor | |||||
// auth credentials here. For now, we just let you enter high | |||||
// security. | |||||
$until = time() + phutil_units('15 minutes in seconds'); | |||||
$session->setHighSecurityUntil($until); | |||||
queryfx( | |||||
$session->establishConnection('w'), | |||||
'UPDATE %T SET highSecurityUntil = %d WHERE id = %d', | |||||
$session->getTableName(), | |||||
$until, | |||||
$session->getID()); | |||||
} | |||||
} | |||||
$token = $this->issueHighSecurityToken($session); | |||||
if ($token) { | |||||
return $token; | |||||
} | |||||
throw id(new PhabricatorAuthHighSecurityRequiredException()) | |||||
->setCancelURI($cancel_uri); | |||||
} | |||||
/** | |||||
* Issue a high security token for a session, if authorized. | |||||
* | |||||
* @param PhabricatorAuthSession Session to issue a token for. | |||||
* @return PhabricatorAuthHighSecurityToken|null Token, if authorized. | |||||
*/ | |||||
private function issueHighSecurityToken(PhabricatorAuthSession $session) { | |||||
$until = $session->getHighSecurityUntil(); | |||||
if ($until > time()) { | |||||
return new PhabricatorAuthHighSecurityToken(); | |||||
} | |||||
return null; | |||||
} | |||||
/** | |||||
* Render a form for providing relevant multi-factor credentials. | |||||
* | |||||
* @param PhabricatorUser Viewing user. | |||||
* @param AphrontRequest Current request. | |||||
* @return AphrontFormView Renderable form. | |||||
*/ | |||||
public function renderHighSecurityForm( | |||||
PhabricatorUser $viewer, | |||||
AphrontRequest $request) { | |||||
// TODO: This is stubbed. | |||||
$form = id(new AphrontFormView()) | |||||
->setUser($viewer) | |||||
->appendRemarkupInstructions('') | |||||
->appendChild( | |||||
id(new AphrontFormTextControl()) | |||||
->setLabel(pht('Secret Stuff'))) | |||||
->appendRemarkupInstructions(''); | |||||
return $form; | |||||
} | |||||
} | } |