From 96555a7385a73c3830a6c1715e19da5ffd509835 Mon Sep 17 00:00:00 2001 From: nupplaPhil Date: Tue, 3 Dec 2019 22:29:37 +0100 Subject: [PATCH] Refactor "Authentication" class with four main methods: - withSession() - for auto authentication with Session/Cookie variables - withOpenId() - for authentication with an OpenID account - withPassword() - for authentication with Password - setForUser() - for setting the user auth context of the current session Refactor "Session" class - contains now "native" Session Management methods --- include/api.php | 5 +- index.php | 3 +- mod/dfrn_poll.php | 5 +- mod/openid.php | 6 +- src/App.php | 6 +- src/Core/Authentication.php | 438 ++++++++++++++++++------------ src/Core/Session.php | 183 +++++-------- src/Module/Delegation.php | 5 +- src/Module/Login.php | 18 +- src/Module/Logout.php | 3 +- src/Module/TwoFactor/Recovery.php | 5 +- src/Module/TwoFactor/Verify.php | 5 +- src/Network/FKOAuth1.php | 5 +- 13 files changed, 386 insertions(+), 301 deletions(-) diff --git a/include/api.php b/include/api.php index 681515cc37..f4b95733e9 100644 --- a/include/api.php +++ b/include/api.php @@ -12,6 +12,7 @@ use Friendica\Content\ContactSelector; use Friendica\Content\Feature; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; +use Friendica\Core\Authentication; use Friendica\Core\Config; use Friendica\Core\Hook; use Friendica\Core\L10n; @@ -253,7 +254,9 @@ function api_login(App $a) throw new UnauthorizedException("This API requires login"); } - Session::setAuthenticatedForUser($a, $record); + /** @var Authentication $authentication */ + $authentication = BaseObject::getClass(Authentication::class); + $authentication->setForUser($a, $record); $_SESSION["allow_api"] = true; diff --git a/index.php b/index.php index 5407532d49..00ec0edb75 100644 --- a/index.php +++ b/index.php @@ -22,5 +22,6 @@ $a = \Friendica\BaseObject::getApp(); $a->runFrontend( $dice->create(\Friendica\App\Module::class), $dice->create(\Friendica\App\Router::class), - $dice->create(\Friendica\Core\Config\PConfiguration::class) + $dice->create(\Friendica\Core\Config\PConfiguration::class), + $dice->create(\Friendica\Core\Authentication::class) ); diff --git a/mod/dfrn_poll.php b/mod/dfrn_poll.php index 6aef31a546..892aecacb7 100644 --- a/mod/dfrn_poll.php +++ b/mod/dfrn_poll.php @@ -5,6 +5,7 @@ */ use Friendica\App; +use Friendica\BaseObject; use Friendica\Core\Authentication; use Friendica\Core\Config; use Friendica\Core\L10n; @@ -21,7 +22,9 @@ use Friendica\Util\XML; function dfrn_poll_init(App $a) { - Authentication::sessionAuth(); + /** @var Authentication $authentication */ + $authentication = BaseObject::getClass(Authentication::class); + $authentication->withSession($a, $_COOKIE); $dfrn_id = $_GET['dfrn_id'] ?? ''; $type = ($_GET['type'] ?? '') ?: 'data'; diff --git a/mod/openid.php b/mod/openid.php index 98748c21d8..0c21f7a31c 100644 --- a/mod/openid.php +++ b/mod/openid.php @@ -4,6 +4,8 @@ */ use Friendica\App; +use Friendica\BaseObject; +use Friendica\Core\Authentication; use Friendica\Core\Config; use Friendica\Core\L10n; use Friendica\Core\Logger; @@ -45,7 +47,9 @@ function openid_content(App $a) { unset($_SESSION['openid']); - Session::setAuthenticatedForUser($a, $user, true, true); + /** @var Authentication $authentication */ + $authentication = BaseObject::getClass(Authentication::class); + $authentication->setForUser($a, $user, true, true); // just in case there was no return url set // and we fell through diff --git a/src/App.php b/src/App.php index 4d652496bf..243f5ba075 100644 --- a/src/App.php +++ b/src/App.php @@ -13,6 +13,7 @@ use Friendica\Core\Config\Cache\ConfigCache; use Friendica\Core\Config\Configuration; use Friendica\Core\Config\PConfiguration; use Friendica\Core\L10n\L10n; +use Friendica\Core\Session; use Friendica\Core\System; use Friendica\Core\Theme; use Friendica\Database\Database; @@ -641,10 +642,11 @@ class App * @param App\Module $module The determined module * @param App\Router $router * @param PConfiguration $pconfig + * @param Authentication $auth The Authentication backend of the node * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public function runFrontend(App\Module $module, App\Router $router, PConfiguration $pconfig) + public function runFrontend(App\Module $module, App\Router $router, PConfiguration $pconfig, Authentication $auth) { $moduleName = $module->getName(); @@ -718,7 +720,7 @@ class App Model\Profile::openWebAuthInit($token); } - Authentication::sessionAuth(); + $auth->withSession($this, $_COOKIE); if (empty($_SESSION['authenticated'])) { header('X-Account-Management-Status: none'); diff --git a/src/Core/Authentication.php b/src/Core/Authentication.php index 6ea727871b..39de73ce02 100644 --- a/src/Core/Authentication.php +++ b/src/Core/Authentication.php @@ -6,77 +6,213 @@ namespace Friendica\Core; +use Exception; use Friendica\App; -use Friendica\BaseObject; +use Friendica\Core\Config\Configuration; +use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Model\User; -use Friendica\Network\HTTPException\ForbiddenException; +use Friendica\Network\HTTPException; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; use Friendica\Util\Strings; +use LightOpenID; +use Friendica\Core\L10n\L10n; +use Psr\Log\LoggerInterface; /** * Handle Authentification, Session and Cookies */ -class Authentication extends BaseObject +class Authentication { + /** @var Configuration */ + private $config; + /** @var App\BaseURL */ + private $baseUrl; + /** @var L10n */ + private $l10n; + /** @var Database */ + private $dba; + /** @var LoggerInterface */ + private $logger; + + /** + * Authentication constructor. + * + * @param Configuration $config + * @param App\BaseURL $baseUrl + * @param L10n $l10n + * @param Database $dba + * @param LoggerInterface $logger + */ + public function __construct(Configuration $config, App\BaseURL $baseUrl, L10n $l10n, Database $dba, LoggerInterface $logger) + { + $this->config = $config; + $this->baseUrl = $baseUrl; + $this->l10n = $l10n; + $this->dba = $dba; + $this->logger = $logger; + } + + /** + * @brief Tries to auth the user from the cookie or session + * + * @param App $a The Friendica Application context + * @param array $cookie The $_COOKIE array + * + * @throws HttpException\InternalServerErrorException In case of Friendica internal exceptions + * @throws Exception In case of general exceptions (like SQL Grammar) + */ + public function withSession(App $a, array $cookie) + { + // When the "Friendica" cookie is set, take the value to authenticate and renew the cookie. + if (isset($cookie["Friendica"])) { + $data = json_decode($cookie["Friendica"]); + if (isset($data->uid)) { + + $user = $this->dba->selectFirst( + 'user', + [], + [ + 'uid' => $data->uid, + 'blocked' => false, + 'account_expired' => false, + 'account_removed' => false, + 'verified' => true, + ] + ); + if (DBA::isResult($user)) { + if (!Session::checkCookie($data->hash, $user)) { + $this->logger->notice("Hash doesn't fit.", ['user' => $data->uid]); + Session::delete(); + $this->baseUrl->redirect(); + } + + // Renew the cookie + // Expires after 7 days by default, + // can be set via system.auth_cookie_lifetime + $authcookiedays = $this->config->get('system', 'auth_cookie_lifetime', 7); + Session::setCookie($authcookiedays * 24 * 60 * 60, $user); + + // Do the authentification if not done by now + if (!Session::get('authenticated')) { + $this->setForUser($a, $user); + + if ($this->config->get('system', 'paranoia')) { + Session::set('addr', $data->ip); + } + } + } + } + } + + if (Session::get('authenticated')) { + if (Session::get('visitor_id') && !Session::get('uid')) { + $contact = $this->dba->selectFirst('contact', [], ['id' => Session::get('visitor_id')]); + if ($this->dba->isResult($contact)) { + $a->contact = $contact; + } + } + + if (Session::get('uid')) { + // already logged in user returning + $check = $this->config->get('system', 'paranoia'); + // extra paranoia - if the IP changed, log them out + if ($check && (Session::get('addr') != $_SERVER['REMOTE_ADDR'])) { + $this->logger->notice('Session address changed. Paranoid setting in effect, blocking session. ', [ + 'addr' => Session::get('addr'), + 'remote_addr' => $_SERVER['REMOTE_ADDR']] + ); + Session::delete(); + $this->baseUrl->redirect(); + } + + $user = $this->dba->selectFirst( + 'user', + [], + [ + 'uid' => Session::get('uid'), + 'blocked' => false, + 'account_expired' => false, + 'account_removed' => false, + 'verified' => true, + ] + ); + if (!$this->dba->isResult($user)) { + Session::delete(); + $this->baseUrl->redirect(); + } + + // Make sure to refresh the last login time for the user if the user + // stays logged in for a long time, e.g. with "Remember Me" + $login_refresh = false; + if (!Session::get('last_login_date')) { + Session::set('last_login_date', DateTimeFormat::utcNow()); + } + if (strcmp(DateTimeFormat::utc('now - 12 hours'), Session::get('last_login_date')) > 0) { + Session::set('last_login_date', DateTimeFormat::utcNow()); + $login_refresh = true; + } + + $this->setForUser($a, $user, false, false, $login_refresh); + } + } + } + /** * Attempts to authenticate using OpenId * * @param string $openid_url OpenID URL string * @param bool $remember Whether to set the session remember flag - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * + * @throws HttpException\InternalServerErrorException In case of Friendica internal exceptions */ - public static function openIdAuthentication($openid_url, $remember) + public function withOpenId(string $openid_url, bool $remember) { - $noid = Config::get('system', 'no_openid'); - - $a = self::getApp(); + $noid = $this->config->get('system', 'no_openid'); // if it's an email address or doesn't resolve to a URL, fail. if ($noid || strpos($openid_url, '@') || !Network::isUrlValid($openid_url)) { - notice(L10n::t('Login failed.') . EOL); - $a->internalRedirect(); - // NOTREACHED + notice($this->l10n->t('Login failed.') . EOL); + $this->baseUrl->redirect(); } // Otherwise it's probably an openid. try { - $openid = new LightOpenID($a->getHostName()); + $openid = new LightOpenID($this->baseUrl->getHostname()); $openid->identity = $openid_url; Session::set('openid', $openid_url); Session::set('remember', $remember); - $openid->returnUrl = $a->getBaseURL(true) . '/openid'; - $openid->optional = ['namePerson/friendly', 'contact/email', 'namePerson', 'namePerson/first', 'media/image/aspect11', 'media/image/default']; + $openid->returnUrl = $this->baseUrl->get(true) . '/openid'; + $openid->optional = ['namePerson/friendly', 'contact/email', 'namePerson', 'namePerson/first', 'media/image/aspect11', 'media/image/default']; System::externalRedirect($openid->authUrl()); } catch (Exception $e) { - notice(L10n::t('We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.') . '

' . L10n::t('The error message was:') . ' ' . $e->getMessage()); + notice($this->l10n->t('We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.') . '

' . $this->l10n->t('The error message was:') . ' ' . $e->getMessage()); } } /** * Attempts to authenticate using login/password * - * @param string $username User name - * @param string $password Clear password - * @param bool $remember Whether to set the session remember flag - * @param string $openid_identity OpenID identity - * @param string $openid_server OpenID URL - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @param App $a The Friendica Application context + * @param string $username User name + * @param string $password Clear password + * @param bool $remember Whether to set the session remember flag + * + * @throws HttpException\InternalServerErrorException In case of Friendica internal exceptions + * @throws Exception A general Exception (like SQL Grammar exceptions) */ - public static function passwordAuthentication($username, $password, $remember, $openid_identity, $openid_server) + public function withPassword(App $a, string $username, string $password, bool $remember) { $record = null; $addon_auth = [ - 'username' => $username, - 'password' => $password, + 'username' => $username, + 'password' => $password, 'authenticated' => 0, - 'user_record' => null + 'user_record' => null ]; - $a = self::getApp(); - /* * An addon indicates successful login by setting 'authenticated' to non-zero value and returning a user record * Addons should never set 'authenticated' except to indicate success - as hooks may be chained @@ -89,199 +225,159 @@ class Authentication extends BaseObject $record = $addon_auth['user_record']; if (empty($record)) { - throw new Exception(L10n::t('Login failed.')); + throw new Exception($this->l10n->t('Login failed.')); } } else { - $record = DBA::selectFirst( + $record = $this->dba->selectFirst( 'user', [], ['uid' => User::getIdFromPasswordAuthentication($username, $password)] ); } } catch (Exception $e) { - Logger::warning('authenticate: failed login attempt', ['action' => 'login', 'username' => Strings::escapeTags($username), 'ip' => $_SERVER['REMOTE_ADDR']]); - info('Login failed. Please check your credentials.' . EOL); - $a->internalRedirect(); + $this->logger->warning('authenticate: failed login attempt', ['action' => 'login', 'username' => Strings::escapeTags($username), 'ip' => $_SERVER['REMOTE_ADDR']]); + info($this->l10n->t('Login failed. Please check your credentials.' . EOL)); + $this->baseUrl->redirect(); } if (!$remember) { - Authentication::setCookie(0); // 0 means delete on browser exit + Session::setCookie(0); // 0 means delete on browser exit } // if we haven't failed up this point, log them in. Session::set('remember', $remember); Session::set('last_login_date', DateTimeFormat::utcNow()); + $openid_identity = Session::get('openid_identity'); + $openid_server = Session::get('openid_server'); + if (!empty($openid_identity) || !empty($openid_server)) { - DBA::update('user', ['openid' => $openid_identity, 'openidserver' => $openid_server], ['uid' => $record['uid']]); + $this->dba->update('user', ['openid' => $openid_identity, 'openidserver' => $openid_server], ['uid' => $record['uid']]); } - Session::setAuthenticatedForUser($a, $record, true, true); + $this->setForUser($a, $record, true, true); $return_path = Session::get('return_path', ''); Session::remove('return_path'); - $a->internalRedirect($return_path); + $this->baseUrl->redirect($return_path); } /** - * @brief Tries to auth the user from the cookie or session + * @brief Sets the provided user's authenticated session * - * @todo Should be moved to Friendica\Core\Session when it's created + * @param App $a The Friendica application context + * @param array $user_record The current "user" record + * @param bool $login_initial + * @param bool $interactive + * @param bool $login_refresh + * + * @throws HTTPException\InternalServerErrorException In case of Friendica specific exceptions + * @throws Exception In case of general Exceptions (like SQL Grammar exceptions) */ - public static function sessionAuth() + public function setForUser(App $a, array $user_record, bool $login_initial = false, bool $interactive = false, bool $login_refresh = false) { - $a = self::getApp(); + Session::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' => $this->baseUrl->get() . '/profile/' . $user_record['nickname'], + 'my_address' => $user_record['nickname'] . '@' . substr($this->baseUrl->get(), strpos($this->baseUrl->get(), '://') + 3), + 'addr' => ($_SERVER['REMOTE_ADDR'] ?? '') ?: '0.0.0.0' + ]); - // When the "Friendica" cookie is set, take the value to authenticate and renew the cookie. - if (isset($_COOKIE["Friendica"])) { - $data = json_decode($_COOKIE["Friendica"]); - if (isset($data->uid)) { + Session::setVisitorsContacts(); - $user = DBA::selectFirst( - 'user', - [], - [ - 'uid' => $data->uid, - 'blocked' => false, - 'account_expired' => false, - 'account_removed' => false, - 'verified' => true, - ] - ); - if (DBA::isResult($user)) { - if (!hash_equals( - Authentication::getCookieHashForUser($user), - $data->hash - )) { - Logger::log("Hash for user " . $data->uid . " doesn't fit."); - Authentication::deleteSession(); - $a->internalRedirect(); - } + $member_since = strtotime($user_record['register_date']); + Session::set('new_member', time() < ($member_since + (60 * 60 * 24 * 14))); - // Renew the cookie - // Expires after 7 days by default, - // can be set via system.auth_cookie_lifetime - $authcookiedays = Config::get('system', 'auth_cookie_lifetime', 7); - Authentication::setCookie($authcookiedays * 24 * 60 * 60, $user); + if (strlen($user_record['timezone'])) { + date_default_timezone_set($user_record['timezone']); + $a->timezone = $user_record['timezone']; + } - // Do the authentification if not done by now - if (!isset($_SESSION) || !isset($_SESSION['authenticated'])) { - Session::setAuthenticatedForUser($a, $user); + $masterUid = $user_record['uid']; - if (Config::get('system', 'paranoia')) { - $_SESSION['addr'] = $data->ip; - } - } - } + if (Session::get('submanage')) { + $user = $this->dba->selectFirst('user', ['uid'], ['uid' => Session::get('submanage')]); + if ($this->dba->isResult($user)) { + $masterUid = $user['uid']; } } - if (!empty($_SESSION['authenticated'])) { - if (!empty($_SESSION['visitor_id']) && empty($_SESSION['uid'])) { - $contact = DBA::selectFirst('contact', [], ['id' => $_SESSION['visitor_id']]); - if (DBA::isResult($contact)) { - self::getApp()->contact = $contact; - } + $a->identities = User::identities($masterUid); + + if ($login_initial) { + $this->logger->info('auth_identities: ' . print_r($a->identities, true)); + } + + if ($login_refresh) { + $this->logger->info('auth_identities refresh: ' . print_r($a->identities, true)); + } + + $contact = $this->dba->selectFirst('contact', [], ['uid' => $user_record['uid'], 'self' => true]); + if ($this->dba->isResult($contact)) { + $a->contact = $contact; + $a->cid = $contact['id']; + Session::set('cid', $a->cid); + } + + header('X-Account-Management-Status: active; name="' . $user_record['username'] . '"; id="' . $user_record['nickname'] . '"'); + + if ($login_initial || $login_refresh) { + $this->dba->update('user', ['login_date' => DateTimeFormat::utcNow()], ['uid' => $user_record['uid']]); + + // Set the login date for all identities of the user + $this->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 (Session::get('remember')) { + $a->getLogger()->info('Injecting cookie for remembered user ' . $user_record['nickname']); + Session::setCookie(604800, $user_record); + Session::remove('remember'); } + } - if (!empty($_SESSION['uid'])) { - // already logged in user returning - $check = Config::get('system', 'paranoia'); - // extra paranoia - if the IP changed, log them out - if ($check && ($_SESSION['addr'] != $_SERVER['REMOTE_ADDR'])) { - Logger::log('Session address changed. Paranoid setting in effect, blocking session. ' . - $_SESSION['addr'] . ' != ' . $_SERVER['REMOTE_ADDR']); - Authentication::deleteSession(); - $a->internalRedirect(); - } + $this->twoFactorCheck($user_record['uid'], $a); - $user = DBA::selectFirst( - 'user', - [], - [ - 'uid' => $_SESSION['uid'], - 'blocked' => false, - 'account_expired' => false, - 'account_removed' => false, - 'verified' => true, - ] - ); - if (!DBA::isResult($user)) { - Authentication::deleteSession(); - $a->internalRedirect(); - } + if ($interactive) { + if ($user_record['login_date'] <= DBA::NULL_DATETIME) { + info($this->l10n->t('Welcome %s', $user_record['username'])); + info($this->l10n->t('Please upload a profile photo.')); + $this->baseUrl->redirect('profile_photo/new'); + } else { + info($this->l10n->t("Welcome back %s", $user_record['username'])); + } + } - // Make sure to refresh the last login time for the user if the user - // stays logged in for a long time, e.g. with "Remember Me" - $login_refresh = false; - if (empty($_SESSION['last_login_date'])) { - $_SESSION['last_login_date'] = DateTimeFormat::utcNow(); - } - if (strcmp(DateTimeFormat::utc('now - 12 hours'), $_SESSION['last_login_date']) > 0) { - $_SESSION['last_login_date'] = DateTimeFormat::utcNow(); - $login_refresh = true; - } + $a->user = $user_record; - Session::setAuthenticatedForUser($a, $user, false, false, $login_refresh); + if ($login_initial) { + Hook::callAll('logged_in', $a->user); + + if ($a->module !== 'home' && Session::exists('return_path')) { + $this->baseUrl->redirect(Session::get('return_path')); } } } /** - * @brief Calculate the hash that is needed for the "Friendica" cookie + * @param int $uid The User Identified + * @param App $a The Friendica Application context * - * @param array $user Record from "user" table - * - * @return string Hashed data - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws HTTPException\ForbiddenException In case the two factor authentication is forbidden (e.g. for AJAX calls) */ - 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) + private function twoFactorCheck(int $uid, App $a) { // Check user setting, if 2FA disabled return if (!PConfig::get($uid, '2fa', 'verified')) { @@ -300,7 +396,7 @@ class Authentication extends BaseObject // Case 2: No valid 2FA session: redirect to code verification page if ($a->isAjax()) { - throw new ForbiddenException(); + throw new HTTPException\ForbiddenException(); } else { $a->internalRedirect('2fa'); } diff --git a/src/Core/Session.php b/src/Core/Session.php index aaead868a0..02e10482d5 100644 --- a/src/Core/Session.php +++ b/src/Core/Session.php @@ -10,8 +10,6 @@ use Friendica\Core\Session\CacheSessionHandler; use Friendica\Core\Session\DatabaseSessionHandler; use Friendica\Database\DBA; use Friendica\Model\Contact; -use Friendica\Model\User; -use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; /** @@ -104,117 +102,11 @@ class Session */ public static function clear() { + session_unset(); + session_start(); $_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')); - } - } - } - /** * Returns contact ID for given user ID * @@ -278,4 +170,75 @@ class Session return $_SESSION['authenticated']; } + + /** + * @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 + */ + private 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 Checks if the "Friendica" cookie is set + * + * @param string $hash + * @param array $user Record from "user" table + * + * @return boolean True, if the cookie is set + * + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function checkCookie(string $hash, array $user) + { + return hash_equals( + self::getCookieHashForUser($user), + $hash + ); + } + + /** + * @brief Kills the "Friendica" cookie and all session data + */ + public static function delete() + { + self::setCookie(-3600); // make sure cookie is deleted on browser close, as a security measure + session_unset(); + session_destroy(); + } } diff --git a/src/Module/Delegation.php b/src/Module/Delegation.php index d2930317c0..2e3a5cafea 100644 --- a/src/Module/Delegation.php +++ b/src/Module/Delegation.php @@ -3,6 +3,7 @@ namespace Friendica\Module; use Friendica\BaseModule; +use Friendica\Core\Authentication; use Friendica\Core\Hook; use Friendica\Core\L10n; use Friendica\Core\Renderer; @@ -79,7 +80,9 @@ class Delegation extends BaseModule Session::clear(); - Session::setAuthenticatedForUser(self::getApp(), $user, true, true); + /** @var Authentication $authentication */ + $authentication = self::getClass(Authentication::class); + $authentication->setForUser(self::getApp(), $user, true, true); if ($limited_id) { Session::set('submanage', $original_id); diff --git a/src/Module/Login.php b/src/Module/Login.php index f43bd221e8..8ecf3e40c8 100644 --- a/src/Module/Login.php +++ b/src/Module/Login.php @@ -35,11 +35,8 @@ class Login extends BaseModule public static function post(array $parameters = []) { - $openid_identity = Session::get('openid_identity'); - $openid_server = Session::get('openid_server'); - $return_path = Session::get('return_path'); - session_unset(); + Session::clear(); Session::set('return_path', $return_path); // OpenId Login @@ -50,16 +47,19 @@ class Login extends BaseModule ) { $openid_url = trim(($_POST['openid_url'] ?? '') ?: $_POST['username']); - Authentication::openIdAuthentication($openid_url, !empty($_POST['remember'])); + /** @var Authentication $authentication */ + $authentication = self::getClass(Authentication::class); + $authentication->withOpenId($openid_url, !empty($_POST['remember'])); } if (!empty($_POST['auth-params']) && $_POST['auth-params'] === 'login') { - Authentication::passwordAuthentication( + /** @var Authentication $authentication */ + $authentication = self::getClass(Authentication::class); + $authentication->withPassword( + self::getApp(), trim($_POST['username']), trim($_POST['password']), - !empty($_POST['remember']), - $openid_identity, - $openid_server + !empty($_POST['remember']) ); } } diff --git a/src/Module/Logout.php b/src/Module/Logout.php index 49ede01a3d..877a8cda05 100644 --- a/src/Module/Logout.php +++ b/src/Module/Logout.php @@ -10,6 +10,7 @@ use Friendica\Core\Authentication; use Friendica\Core\Cache; use Friendica\Core\Hook; use Friendica\Core\L10n; +use Friendica\Core\Session; use Friendica\Core\System; use Friendica\Model\Profile; @@ -32,7 +33,7 @@ class Logout extends BaseModule } Hook::callAll("logging_out"); - Authentication::deleteSession(); + Session::delete(); if ($visitor_home) { System::externalRedirect($visitor_home); diff --git a/src/Module/TwoFactor/Recovery.php b/src/Module/TwoFactor/Recovery.php index bd87836463..f1454469f2 100644 --- a/src/Module/TwoFactor/Recovery.php +++ b/src/Module/TwoFactor/Recovery.php @@ -3,6 +3,7 @@ namespace Friendica\Module\TwoFactor; use Friendica\BaseModule; +use Friendica\Core\Authentication; use Friendica\Core\L10n; use Friendica\Core\Renderer; use Friendica\Core\Session; @@ -41,7 +42,9 @@ class Recovery extends BaseModule notice(L10n::t('Remaining recovery codes: %d', RecoveryCode::countValidForUser(local_user()))); // Resume normal login workflow - Session::setAuthenticatedForUser($a, $a->user, true, true); + /** @var Authentication $authentication */ + $authentication = self::getClass(Authentication::class); + $authentication->setForUser($a, $a->user, true, true); } else { notice(L10n::t('Invalid code, please retry.')); } diff --git a/src/Module/TwoFactor/Verify.php b/src/Module/TwoFactor/Verify.php index 27001683e9..e4a0b2ff18 100644 --- a/src/Module/TwoFactor/Verify.php +++ b/src/Module/TwoFactor/Verify.php @@ -3,6 +3,7 @@ namespace Friendica\Module\TwoFactor; use Friendica\BaseModule; +use Friendica\Core\Authentication; use Friendica\Core\L10n; use Friendica\Core\PConfig; use Friendica\Core\Renderer; @@ -38,7 +39,9 @@ class Verify extends BaseModule Session::set('2fa', $code); // Resume normal login workflow - Session::setAuthenticatedForUser($a, $a->user, true, true); + /** @var Authentication $authentication */ + $authentication = self::getClass(Authentication::class); + $authentication->setForUser($a, $a->user, true, true); } else { self::$errors[] = L10n::t('Invalid code, please retry.'); } diff --git a/src/Network/FKOAuth1.php b/src/Network/FKOAuth1.php index eb13290577..81c0c1b291 100644 --- a/src/Network/FKOAuth1.php +++ b/src/Network/FKOAuth1.php @@ -5,6 +5,7 @@ namespace Friendica\Network; use Friendica\BaseObject; +use Friendica\Core\Authentication; use Friendica\Core\Logger; use Friendica\Core\Session; use Friendica\Database\DBA; @@ -45,6 +46,8 @@ class FKOAuth1 extends OAuthServer die('This api requires login'); } - Session::setAuthenticatedForUser($a, $record, true); + /** @var Authentication $authentication */ + $authentication = BaseObject::getClass(Authentication::class); + $authentication->setForUser($a, $record, true); } }