1
0
Fork 0

Merge pull request #7907 from nupplaphil/task/reduce_app_deps

Cleanup Session/Authentication
This commit is contained in:
Hypolite Petovan 2019-12-14 09:53:40 -05:00 committed by GitHub
commit 6e4a428c73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 8791 additions and 7936 deletions

View file

@ -1,95 +0,0 @@
<?php
/**
* @file /src/Core/Authentication.php
*/
namespace Friendica\Core;
use Friendica\App;
use Friendica\BaseObject;
use Friendica\Network\HTTPException\ForbiddenException;
/**
* Handle Authentification, Session and Cookies
*/
class Authentication extends BaseObject
{
/**
* @brief Calculate the hash that is needed for the "Friendica" cookie
*
* @param array $user Record from "user" table
*
* @return string Hashed data
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getCookieHashForUser($user)
{
return hash_hmac(
"sha256",
hash_hmac("sha256", $user["password"], $user["prvkey"]),
Config::get("system", "site_prvkey")
);
}
/**
* @brief Set the "Friendica" cookie
*
* @param int $time
* @param array $user Record from "user" table
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function setCookie($time, $user = [])
{
if ($time != 0) {
$time = $time + time();
}
if ($user) {
$value = json_encode([
"uid" => $user["uid"],
"hash" => self::getCookieHashForUser($user),
"ip" => ($_SERVER['REMOTE_ADDR'] ?? '') ?: '0.0.0.0'
]);
} else {
$value = "";
}
setcookie("Friendica", $value, $time, "/", "", (Config::get('system', 'ssl_policy') == App\BaseURL::SSL_POLICY_FULL), true);
}
/**
* @brief Kills the "Friendica" cookie and all session data
*/
public static function deleteSession()
{
self::setCookie(-3600); // make sure cookie is deleted on browser close, as a security measure
session_unset();
session_destroy();
}
public static function twoFactorCheck($uid, App $a)
{
// Check user setting, if 2FA disabled return
if (!PConfig::get($uid, '2fa', 'verified')) {
return;
}
// Check current path, if 2fa authentication module return
if ($a->argc > 0 && in_array($a->argv[0], ['2fa', 'view', 'help', 'api', 'proxy', 'logout'])) {
return;
}
// Case 1: 2FA session present and valid: return
if (Session::get('2fa')) {
return;
}
// Case 2: No valid 2FA session: redirect to code verification page
if ($a->isAjax()) {
throw new ForbiddenException();
} else {
$a->internalRedirect('2fa');
}
}
}

View file

@ -4,7 +4,7 @@ namespace Friendica\Core\L10n;
use Friendica\Core\Config\Configuration;
use Friendica\Core\Hook;
use Friendica\Core\Session;
use Friendica\Core\Session\ISession;
use Friendica\Database\Database;
use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
@ -53,12 +53,14 @@ class L10n
*/
private $logger;
public function __construct(Configuration $config, Database $dba, LoggerInterface $logger, array $server, array $get)
public function __construct(Configuration $config, Database $dba, LoggerInterface $logger, ISession $session, array $server, array $get)
{
$this->dba = $dba;
$this->logger = $logger;
$this->loadTranslationTable(L10n::detectLanguage($server, $get, $config->get('system', 'language', 'en')));
$this->setSessionVariable($session);
$this->setLangFromSession($session);
}
/**
@ -74,28 +76,28 @@ class L10n
/**
* Sets the language session variable
*/
public function setSessionVariable()
private function setSessionVariable(ISession $session)
{
if (Session::get('authenticated') && !Session::get('language')) {
$_SESSION['language'] = $this->lang;
if ($session->get('authenticated') && !$session->get('language')) {
$session->set('language', $this->lang);
// we haven't loaded user data yet, but we need user language
if (Session::get('uid')) {
if ($session->get('uid')) {
$user = $this->dba->selectFirst('user', ['language'], ['uid' => $_SESSION['uid']]);
if ($this->dba->isResult($user)) {
$_SESSION['language'] = $user['language'];
$session->set('language', $user['language']);
}
}
}
if (isset($_GET['lang'])) {
Session::set('language', $_GET['lang']);
$session->set('language', $_GET['lang']);
}
}
public function setLangFromSession()
private function setLangFromSession(ISession $session)
{
if (Session::get('language') !== $this->lang) {
$this->loadTranslationTable(Session::get('language'));
if ($session->get('language') !== $this->lang) {
$this->loadTranslationTable($session->get('language'));
}
}

View file

@ -5,13 +5,10 @@
*/
namespace Friendica\Core;
use Friendica\App;
use Friendica\Core\Session\CacheSessionHandler;
use Friendica\Core\Session\DatabaseSessionHandler;
use Friendica\BaseObject;
use Friendica\Core\Session\ISession;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Model\User;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings;
/**
@ -19,200 +16,39 @@ use Friendica\Util\Strings;
*
* @author Hypolite Petovan <hypolite@mrpetovan.com>
*/
class Session
class Session extends BaseObject
{
public static $exists = false;
public static $expire = 180000;
public static function init()
{
ini_set('session.gc_probability', 50);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_httponly', 1);
if (Config::get('system', 'ssl_policy') == App\BaseURL::SSL_POLICY_FULL) {
ini_set('session.cookie_secure', 1);
}
$session_handler = Config::get('system', 'session_handler', 'database');
if ($session_handler != 'native') {
if ($session_handler == 'cache' && Config::get('system', 'cache_driver', 'database') != 'database') {
$SessionHandler = new CacheSessionHandler();
} else {
$SessionHandler = new DatabaseSessionHandler();
}
session_set_save_handler($SessionHandler);
}
}
public static function exists($name)
{
return isset($_SESSION[$name]);
return self::getClass(ISession::class)->exists($name);
}
/**
* Retrieves a key from the session super global or the defaults if the key is missing or the value is falsy.
*
* Handle the case where session_start() hasn't been called and the super global isn't available.
*
* @param string $name
* @param mixed $defaults
* @return mixed
*/
public static function get($name, $defaults = null)
{
return $_SESSION[$name] ?? $defaults;
return self::getClass(ISession::class)->get($name, $defaults);
}
/**
* Sets a single session variable.
* Overrides value of existing key.
*
* @param string $name
* @param mixed $value
*/
public static function set($name, $value)
{
$_SESSION[$name] = $value;
self::getClass(ISession::class)->set($name, $value);
}
/**
* Sets multiple session variables.
* Overrides values for existing keys.
*
* @param array $values
*/
public static function setMultiple(array $values)
{
$_SESSION = $values + $_SESSION;
self::getClass(ISession::class)->setMultiple($values);
}
/**
* Removes a session variable.
* Ignores missing keys.
*
* @param $name
*/
public static function remove($name)
{
unset($_SESSION[$name]);
self::getClass(ISession::class)->remove($name);
}
/**
* Clears the current session array
*/
public static function clear()
{
$_SESSION = [];
}
/**
* @brief Sets the provided user's authenticated session
*
* @param App $a
* @param array $user_record
* @param bool $login_initial
* @param bool $interactive
* @param bool $login_refresh
* @throws \Friendica\Network\HTTPException\ForbiddenException
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function setAuthenticatedForUser(App $a, array $user_record, $login_initial = false, $interactive = false, $login_refresh = false)
{
self::setMultiple([
'uid' => $user_record['uid'],
'theme' => $user_record['theme'],
'mobile-theme' => PConfig::get($user_record['uid'], 'system', 'mobile_theme'),
'authenticated' => 1,
'page_flags' => $user_record['page-flags'],
'my_url' => $a->getBaseURL() . '/profile/' . $user_record['nickname'],
'my_address' => $user_record['nickname'] . '@' . substr($a->getBaseURL(), strpos($a->getBaseURL(), '://') + 3),
'addr' => ($_SERVER['REMOTE_ADDR'] ?? '') ?: '0.0.0.0'
]);
self::setVisitorsContacts();
$member_since = strtotime($user_record['register_date']);
self::set('new_member', time() < ($member_since + ( 60 * 60 * 24 * 14)));
if (strlen($user_record['timezone'])) {
date_default_timezone_set($user_record['timezone']);
$a->timezone = $user_record['timezone'];
}
$masterUid = $user_record['uid'];
if (self::get('submanage')) {
$user = DBA::selectFirst('user', ['uid'], ['uid' => self::get('submanage')]);
if (DBA::isResult($user)) {
$masterUid = $user['uid'];
}
}
$a->identities = User::identities($masterUid);
if ($login_initial) {
$a->getLogger()->info('auth_identities: ' . print_r($a->identities, true));
}
if ($login_refresh) {
$a->getLogger()->info('auth_identities refresh: ' . print_r($a->identities, true));
}
$contact = DBA::selectFirst('contact', [], ['uid' => $user_record['uid'], 'self' => true]);
if (DBA::isResult($contact)) {
$a->contact = $contact;
$a->cid = $contact['id'];
self::set('cid', $a->cid);
}
header('X-Account-Management-Status: active; name="' . $user_record['username'] . '"; id="' . $user_record['nickname'] . '"');
if ($login_initial || $login_refresh) {
DBA::update('user', ['login_date' => DateTimeFormat::utcNow()], ['uid' => $user_record['uid']]);
// Set the login date for all identities of the user
DBA::update('user', ['login_date' => DateTimeFormat::utcNow()],
['parent-uid' => $masterUid, 'account_removed' => false]);
}
if ($login_initial) {
/*
* If the user specified to remember the authentication, then set a cookie
* that expires after one week (the default is when the browser is closed).
* The cookie will be renewed automatically.
* The week ensures that sessions will expire after some inactivity.
*/
;
if (self::get('remember')) {
$a->getLogger()->info('Injecting cookie for remembered user ' . $user_record['nickname']);
Authentication::setCookie(604800, $user_record);
self::remove('remember');
}
}
Authentication::twoFactorCheck($user_record['uid'], $a);
if ($interactive) {
if ($user_record['login_date'] <= DBA::NULL_DATETIME) {
info(L10n::t('Welcome %s', $user_record['username']));
info(L10n::t('Please upload a profile photo.'));
$a->internalRedirect('profile_photo/new');
} else {
info(L10n::t("Welcome back %s", $user_record['username']));
}
}
$a->user = $user_record;
if ($login_initial) {
Hook::callAll('logged_in', $a->user);
if ($a->module !== 'home' && self::exists('return_path')) {
$a->internalRedirect(self::get('return_path'));
}
}
self::getClass(ISession::class)->clear();
}
/**
@ -223,11 +59,14 @@ class Session
*/
public static function getRemoteContactID($uid)
{
if (empty($_SESSION['remote'][$uid])) {
/** @var ISession $session */
$session = self::getClass(ISession::class);
if (empty($session->get('remote')[$uid])) {
return false;
}
return $_SESSION['remote'][$uid];
return $session->get('remote')[$uid];
}
/**
@ -238,11 +77,14 @@ class Session
*/
public static function getUserIDForVisitorContactID($cid)
{
if (empty($_SESSION['remote'])) {
/** @var ISession $session */
$session = self::getClass(ISession::class);
if (empty($session->get('remote'))) {
return false;
}
return array_search($cid, $_SESSION['remote']);
return array_search($cid, $session->get('remote'));
}
/**
@ -252,15 +94,18 @@ class Session
*/
public static function setVisitorsContacts()
{
$_SESSION['remote'] = [];
/** @var ISession $session */
$session = self::getClass(ISession::class);
$remote_contacts = DBA::select('contact', ['id', 'uid'], ['nurl' => Strings::normaliseLink($_SESSION['my_url']), 'rel' => [Contact::FOLLOWER, Contact::FRIEND], 'self' => false]);
$session->set('remote', []);
$remote_contacts = DBA::select('contact', ['id', 'uid'], ['nurl' => Strings::normaliseLink($session->get('my_url')), 'rel' => [Contact::FOLLOWER, Contact::FRIEND], 'self' => false]);
while ($contact = DBA::fetch($remote_contacts)) {
if (($contact['uid'] == 0) || Contact::isBlockedByUser($contact['id'], $contact['uid'])) {
continue;
}
$_SESSION['remote'][$contact['uid']] = $contact['id'];
$session->set('remote', [$contact['uid'] => $contact['id']]);
}
DBA::close($remote_contacts);
}
@ -272,10 +117,9 @@ class Session
*/
public static function isAuthenticated()
{
if (empty($_SESSION['authenticated'])) {
return false;
}
/** @var ISession $session */
$session = self::getClass(ISession::class);
return $_SESSION['authenticated'];
return $session->get('authenticated', false);
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Friendica\Core\Session;
use Friendica\Model\User\Cookie;
/**
* Contains the base methods for $_SESSION interaction
*/
class AbstractSession
{
/** @var Cookie */
protected $cookie;
public function __construct( Cookie $cookie)
{
$this->cookie = $cookie;
}
/**
* {@inheritDoc}
*/
public function start()
{
return $this;
}
/**
* {@inheritDoc}}
*/
public function exists(string $name)
{
return isset($_SESSION[$name]);
}
/**
* {@inheritDoc}
*/
public function get(string $name, $defaults = null)
{
return $_SESSION[$name] ?? $defaults;
}
/**
* {@inheritDoc}
*/
public function set(string $name, $value)
{
$_SESSION[$name] = $value;
}
/**
* {@inheritDoc}
*/
public function setMultiple(array $values)
{
$_SESSION = $values + $_SESSION;
}
/**
* {@inheritDoc}
*/
public function remove(string $name)
{
unset($_SESSION[$name]);
}
/**
* {@inheritDoc}
*/
public function clear()
{
$_SESSION = [];
}
}

View file

@ -1,93 +0,0 @@
<?php
namespace Friendica\Core\Session;
use Friendica\BaseObject;
use Friendica\Core\Logger;
use Friendica\Core\Session;
use Friendica\Database\DBA;
use SessionHandlerInterface;
/**
* SessionHandler using database
*
* @author Hypolite Petovan <hypolite@mrpetovan.com>
*/
class DatabaseSessionHandler extends BaseObject implements SessionHandlerInterface
{
public function open($save_path, $session_name)
{
return true;
}
public function read($session_id)
{
if (empty($session_id)) {
return '';
}
$session = DBA::selectFirst('session', ['data'], ['sid' => $session_id]);
if (DBA::isResult($session)) {
Session::$exists = true;
return $session['data'];
}
Logger::notice('no data for session', ['session_id' => $session_id, 'uri' => $_SERVER['REQUEST_URI']]);
return '';
}
/**
* @brief Standard PHP session write callback
*
* This callback updates the DB-stored session data and/or the expiration depending
* on the case. Uses the Session::expire global for existing session, 5 minutes
* for newly created session.
*
* @param string $session_id Session ID with format: [a-z0-9]{26}
* @param string $session_data Serialized session data
* @return boolean Returns false if parameters are missing, true otherwise
* @throws \Exception
*/
public function write($session_id, $session_data)
{
if (!$session_id) {
return false;
}
if (!$session_data) {
return true;
}
$expire = time() + Session::$expire;
$default_expire = time() + 300;
if (Session::$exists) {
$fields = ['data' => $session_data, 'expire' => $expire];
$condition = ["`sid` = ? AND (`data` != ? OR `expire` != ?)", $session_id, $session_data, $expire];
DBA::update('session', $fields, $condition);
} else {
$fields = ['sid' => $session_id, 'expire' => $default_expire, 'data' => $session_data];
DBA::insert('session', $fields);
}
return true;
}
public function close()
{
return true;
}
public function destroy($id)
{
DBA::delete('session', ['sid' => $id]);
return true;
}
public function gc($maxlifetime)
{
DBA::delete('session', ["`expire` < ?", time()]);
return true;
}
}

View file

@ -1,11 +1,10 @@
<?php
namespace Friendica\Core\Session;
namespace Friendica\Core\Session\Handler;
use Friendica\BaseObject;
use Friendica\Core\Cache;
use Friendica\Core\Logger;
use Friendica\Core\Cache\ICache;
use Friendica\Core\Session;
use Psr\Log\LoggerInterface;
use SessionHandlerInterface;
/**
@ -13,8 +12,22 @@ use SessionHandlerInterface;
*
* @author Hypolite Petovan <hypolite@mrpetovan.com>
*/
class CacheSessionHandler extends BaseObject implements SessionHandlerInterface
final class Cache implements SessionHandlerInterface
{
/** @var ICache */
private $cache;
/** @var LoggerInterface */
private $logger;
/** @var array The $_SERVER array */
private $server;
public function __construct(ICache $cache, LoggerInterface $logger, array $server)
{
$this->cache = $cache;
$this->logger = $logger;
$this->server = $server;
}
public function open($save_path, $session_name)
{
return true;
@ -26,13 +39,13 @@ class CacheSessionHandler extends BaseObject implements SessionHandlerInterface
return '';
}
$data = Cache::get('session:' . $session_id);
$data = $this->cache->get('session:' . $session_id);
if (!empty($data)) {
Session::$exists = true;
return $data;
}
Logger::notice('no data for session', ['session_id' => $session_id, 'uri' => $_SERVER['REQUEST_URI']]);
$this->logger->notice('no data for session', ['session_id' => $session_id, 'uri' => $this->server['REQUEST_URI'] ?? '']);
return '';
}
@ -44,8 +57,9 @@ class CacheSessionHandler extends BaseObject implements SessionHandlerInterface
* on the case. Uses the Session::expire for existing session, 5 minutes
* for newly created session.
*
* @param string $session_id Session ID with format: [a-z0-9]{26}
* @param string $session_data Serialized session data
* @param string $session_id Session ID with format: [a-z0-9]{26}
* @param string $session_data Serialized session data
*
* @return boolean Returns false if parameters are missing, true otherwise
* @throws \Exception
*/
@ -59,9 +73,7 @@ class CacheSessionHandler extends BaseObject implements SessionHandlerInterface
return true;
}
$return = Cache::set('session:' . $session_id, $session_data, Session::$expire);
return $return;
return $this->cache->set('session:' . $session_id, $session_data, Session::$expire);
}
public function close()
@ -71,9 +83,7 @@ class CacheSessionHandler extends BaseObject implements SessionHandlerInterface
public function destroy($id)
{
$return = Cache::delete('session:' . $id);
return $return;
return $this->cache->delete('session:' . $id);
}
public function gc($maxlifetime)

View file

@ -0,0 +1,112 @@
<?php
namespace Friendica\Core\Session\Handler;
use Friendica\Core\Session;
use Friendica\Database\Database as DBA;
use Psr\Log\LoggerInterface;
use SessionHandlerInterface;
/**
* SessionHandler using database
*
* @author Hypolite Petovan <hypolite@mrpetovan.com>
*/
final class Database implements SessionHandlerInterface
{
/** @var DBA */
private $dba;
/** @var LoggerInterface */
private $logger;
/** @var array The $_SERVER variable */
private $server;
/**
* DatabaseSessionHandler constructor.
*
* @param DBA $dba
* @param LoggerInterface $logger
* @param array $server
*/
public function __construct(DBA $dba, LoggerInterface $logger, array $server)
{
$this->dba = $dba;
$this->logger = $logger;
$this->server = $server;
}
public function open($save_path, $session_name)
{
return true;
}
public function read($session_id)
{
if (empty($session_id)) {
return '';
}
$session = $this->dba->selectFirst('session', ['data'], ['sid' => $session_id]);
if ($this->dba->isResult($session)) {
Session::$exists = true;
return $session['data'];
}
$this->logger->notice('no data for session', ['session_id' => $session_id, 'uri' => $this->server['REQUEST_URI'] ?? '']);
return '';
}
/**
* @brief Standard PHP session write callback
*
* This callback updates the DB-stored session data and/or the expiration depending
* on the case. Uses the Session::expire global for existing session, 5 minutes
* for newly created session.
*
* @param string $session_id Session ID with format: [a-z0-9]{26}
* @param string $session_data Serialized session data
*
* @return boolean Returns false if parameters are missing, true otherwise
* @throws \Exception
*/
public function write($session_id, $session_data)
{
if (!$session_id) {
return false;
}
if (!$session_data) {
return true;
}
$expire = time() + Session::$expire;
$default_expire = time() + 300;
if (Session::$exists) {
$fields = ['data' => $session_data, 'expire' => $expire];
$condition = ["`sid` = ? AND (`data` != ? OR `expire` != ?)", $session_id, $session_data, $expire];
$this->dba->update('session', $fields, $condition);
} else {
$fields = ['sid' => $session_id, 'expire' => $default_expire, 'data' => $session_data];
$this->dba->insert('session', $fields);
}
return true;
}
public function close()
{
return true;
}
public function destroy($id)
{
return $this->dba->delete('session', ['sid' => $id]);
}
public function gc($maxlifetime)
{
return $this->dba->delete('session', ["`expire` < ?", time()]);
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Friendica\Core\Session;
/**
* Contains all global supported Session methods
*/
interface ISession
{
/**
* Start the current session
*
* @return self The own Session instance
*/
public function start();
/**
* Checks if the key exists in this session
*
* @param string $name
*
* @return boolean True, if it exists
*/
public function exists(string $name);
/**
* Retrieves a key from the session super global or the defaults if the key is missing or the value is falsy.
*
* Handle the case where session_start() hasn't been called and the super global isn't available.
*
* @param string $name
* @param mixed $defaults
*
* @return mixed
*/
public function get(string $name, $defaults = null);
/**
* Sets a single session variable.
* Overrides value of existing key.
*
* @param string $name
* @param mixed $value
*/
public function set(string $name, $value);
/**
* Sets multiple session variables.
* Overrides values for existing keys.
*
* @param array $values
*/
public function setMultiple(array $values);
/**
* Removes a session variable.
* Ignores missing keys.
*
* @param string $name
*/
public function remove(string $name);
/**
* Clears the current session array
*/
public function clear();
}

View file

@ -0,0 +1,22 @@
<?php
namespace Friendica\Core\Session;
use Friendica\Model\User\Cookie;
/**
* Usable for backend processes (daemon/worker) and testing
*
* @todo after replacing the last direct $_SESSION call, use a internal array instead of the global variable
*/
final class Memory extends AbstractSession implements ISession
{
public function __construct(Cookie $cookie)
{
parent::__construct($cookie);
// Backward compatibility until all Session variables are replaced
// with the Session class
$_SESSION = [];
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Friendica\Core\Session;
use Friendica\App;
use Friendica\Model\User\Cookie;
use SessionHandlerInterface;
/**
* The native Session class which uses the PHP internal Session functions
*/
final class Native extends AbstractSession implements ISession
{
public function __construct(App\BaseURL $baseURL, Cookie $cookie, SessionHandlerInterface $handler = null)
{
parent::__construct($cookie);
ini_set('session.gc_probability', 50);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_httponly', (int)Cookie::HTTPONLY);
if ($baseURL->getSSLPolicy() == App\BaseURL::SSL_POLICY_FULL) {
ini_set('session.cookie_secure', 1);
}
if (isset($handler)) {
session_set_save_handler($handler);
}
}
/**
* {@inheritDoc}
*/
public function start()
{
session_start();
return $this;
}
}