From: Philipp Date: Wed, 30 Sep 2020 09:14:01 +0000 (+0200) Subject: Move ExAuth, FKOAuth1 & FKOAuthDataStore to own namespace `Friendica\Security` X-Git-Tag: 2021.01~235^2~4 X-Git-Url: https://reisub.nsupdate.info/git/?a=commitdiff_plain;h=8318a0b6407a1e76ebe09f3cd9a4349382235319;p=friendica.git%2F.git Move ExAuth, FKOAuth1 & FKOAuthDataStore to own namespace `Friendica\Security` --- diff --git a/bin/auth_ejabberd.php b/bin/auth_ejabberd.php index e921829163..d6e20dfe15 100755 --- a/bin/auth_ejabberd.php +++ b/bin/auth_ejabberd.php @@ -58,7 +58,7 @@ if (php_sapi_name() !== 'cli') { use Dice\Dice; use Friendica\App\Mode; -use Friendica\Util\ExAuth; +use Friendica\Security\ExAuth; use Psr\Log\LoggerInterface; if (sizeof($_SERVER["argv"]) == 0) { diff --git a/library/OAuth1.php b/library/OAuth1.php index 813234b67b..05ec5aa755 100644 --- a/library/OAuth1.php +++ b/library/OAuth1.php @@ -4,7 +4,7 @@ /* Generic exception class */ -use Friendica\Network\FKOAuthDataStore; +use Friendica\Security\FKOAuthDataStore; if (!class_exists('OAuthException', false)) { class OAuthException extends Exception diff --git a/mod/item.php b/mod/item.php index 17f6486f89..b77e725756 100644 --- a/mod/item.php +++ b/mod/item.php @@ -54,7 +54,7 @@ use Friendica\Object\EMail\ItemCCEMail; use Friendica\Protocol\Activity; use Friendica\Protocol\Diaspora; use Friendica\Util\DateTimeFormat; -use Friendica\Util\Security; +use Friendica\Security\Security; use Friendica\Util\Strings; use Friendica\Worker\Delivery; diff --git a/mod/photos.php b/mod/photos.php index 3b2fa0c3a8..bba12aaceb 100644 --- a/mod/photos.php +++ b/mod/photos.php @@ -47,7 +47,7 @@ use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\Images; use Friendica\Util\Map; -use Friendica\Util\Security; +use Friendica\Security\Security; use Friendica\Util\Strings; use Friendica\Util\Temporal; use Friendica\Util\XML; diff --git a/mod/videos.php b/mod/videos.php index 3dd17179ae..1ba566eeaa 100644 --- a/mod/videos.php +++ b/mod/videos.php @@ -33,7 +33,7 @@ use Friendica\Model\Item; use Friendica\Model\Profile; use Friendica\Model\User; use Friendica\Module\BaseProfile; -use Friendica\Util\Security; +use Friendica\Security\Security; function videos_init(App $a) { diff --git a/src/Model/Attach.php b/src/Model/Attach.php index ad587e68bd..b81c38762e 100644 --- a/src/Model/Attach.php +++ b/src/Model/Attach.php @@ -28,7 +28,7 @@ use Friendica\DI; use Friendica\Object\Image; use Friendica\Util\DateTimeFormat; use Friendica\Util\Mimetype; -use Friendica\Util\Security; +use Friendica\Security\Security; /** * Class to handle attach dabatase table diff --git a/src/Model/Photo.php b/src/Model/Photo.php index f09e88ce7d..6380f42789 100644 --- a/src/Model/Photo.php +++ b/src/Model/Photo.php @@ -31,7 +31,7 @@ use Friendica\Model\Storage\SystemResource; use Friendica\Object\Image; use Friendica\Util\DateTimeFormat; use Friendica\Util\Images; -use Friendica\Util\Security; +use Friendica\Security\Security; use Friendica\Util\Strings; require_once "include/dba.php"; diff --git a/src/Module/Profile/Status.php b/src/Module/Profile/Status.php index 421c8acccd..fbc287e6a8 100644 --- a/src/Module/Profile/Status.php +++ b/src/Module/Profile/Status.php @@ -36,7 +36,7 @@ use Friendica\Module\BaseProfile; use Friendica\Module\Security\Login; use Friendica\Network\HTTPException; use Friendica\Util\DateTimeFormat; -use Friendica\Util\Security; +use Friendica\Security\Security; use Friendica\Util\Strings; use Friendica\Util\XML; diff --git a/src/Network/FKOAuth1.php b/src/Network/FKOAuth1.php deleted file mode 100644 index 9833d5e0af..0000000000 --- a/src/Network/FKOAuth1.php +++ /dev/null @@ -1,66 +0,0 @@ -. - * - */ - -namespace Friendica\Network; - -use Friendica\Core\Logger; -use Friendica\Database\DBA; -use Friendica\DI; -use OAuthServer; -use OAuthSignatureMethod_HMAC_SHA1; -use OAuthSignatureMethod_PLAINTEXT; - -/** - * OAuth protocol - */ -class FKOAuth1 extends OAuthServer -{ - /** - * Constructor - */ - public function __construct() - { - parent::__construct(new FKOAuthDataStore()); - $this->add_signature_method(new OAuthSignatureMethod_PLAINTEXT()); - $this->add_signature_method(new OAuthSignatureMethod_HMAC_SHA1()); - } - - /** - * @param string $uid user id - * @return void - * @throws HTTPException\ForbiddenException - * @throws HTTPException\InternalServerErrorException - */ - public function loginUser($uid) - { - Logger::notice("FKOAuth1::loginUser $uid"); - $a = DI::app(); - $record = DBA::selectFirst('user', [], ['uid' => $uid, 'blocked' => 0, 'account_expired' => 0, 'account_removed' => 0, 'verified' => 1]); - - if (!DBA::isResult($record)) { - Logger::info('FKOAuth1::loginUser failure', ['server' => $_SERVER]); - header('HTTP/1.0 401 Unauthorized'); - die('This api requires login'); - } - - DI::auth()->setForUser($a, $record, true); - } -} diff --git a/src/Network/FKOAuthDataStore.php b/src/Network/FKOAuthDataStore.php deleted file mode 100644 index ee9a709152..0000000000 --- a/src/Network/FKOAuthDataStore.php +++ /dev/null @@ -1,197 +0,0 @@ -. - * - */ - -namespace Friendica\Network; - -use Friendica\Core\Logger; -use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Util\Strings; -use OAuthConsumer; -use OAuthDataStore; -use OAuthToken; - -define('REQUEST_TOKEN_DURATION', 300); -define('ACCESS_TOKEN_DURATION', 31536000); - -/** - * OAuthDataStore class - */ -class FKOAuthDataStore extends OAuthDataStore -{ - /** - * @return string - * @throws \Exception - */ - private static function genToken() - { - return Strings::getRandomHex(32); - } - - /** - * @param string $consumer_key key - * @return OAuthConsumer|null - * @throws \Exception - */ - public function lookup_consumer($consumer_key) - { - Logger::log(__function__ . ":" . $consumer_key); - - $s = DBA::select('clients', ['client_id', 'pw', 'redirect_uri'], ['client_id' => $consumer_key]); - $r = DBA::toArray($s); - - if (DBA::isResult($r)) { - return new OAuthConsumer($r[0]['client_id'], $r[0]['pw'], $r[0]['redirect_uri']); - } - - return null; - } - - /** - * @param OAuthConsumer $consumer - * @param string $token_type - * @param string $token_id - * @return OAuthToken|null - * @throws \Exception - */ - public function lookup_token(OAuthConsumer $consumer, $token_type, $token_id) - { - Logger::log(__function__ . ":" . $consumer . ", " . $token_type . ", " . $token_id); - - $s = DBA::select('tokens', ['id', 'secret', 'scope', 'expires', 'uid'], ['client_id' => $consumer->key, 'scope' => $token_type, 'id' => $token_id]); - $r = DBA::toArray($s); - - if (DBA::isResult($r)) { - $ot = new OAuthToken($r[0]['id'], $r[0]['secret']); - $ot->scope = $r[0]['scope']; - $ot->expires = $r[0]['expires']; - $ot->uid = $r[0]['uid']; - return $ot; - } - - return null; - } - - /** - * @param OAuthConsumer $consumer - * @param OAuthToken $token - * @param string $nonce - * @param int $timestamp - * @return mixed - * @throws \Exception - */ - public function lookup_nonce(OAuthConsumer $consumer, OAuthToken $token, $nonce, int $timestamp) - { - $token = DBA::selectFirst('tokens', ['id', 'secret'], ['client_id' => $consumer->key, 'id' => $nonce, 'expires' => $timestamp]); - if (DBA::isResult($token)) { - return new OAuthToken($token['id'], $token['secret']); - } - - return null; - } - - /** - * @param OAuthConsumer $consumer - * @param string $callback - * @return OAuthToken|null - * @throws \Exception - */ - public function new_request_token(OAuthConsumer $consumer, $callback = null) - { - Logger::log(__function__ . ":" . $consumer . ", " . $callback); - $key = self::genToken(); - $sec = self::genToken(); - - if ($consumer->key) { - $k = $consumer->key; - } else { - $k = $consumer; - } - - $r = DBA::insert( - 'tokens', - [ - 'id' => $key, - 'secret' => $sec, - 'client_id' => $k, - 'scope' => 'request', - 'expires' => time() + REQUEST_TOKEN_DURATION - ] - ); - - if (!$r) { - return null; - } - - return new OAuthToken($key, $sec); - } - - /** - * @param OAuthToken $token token - * @param OAuthConsumer $consumer consumer - * @param string $verifier optional, defult null - * @return OAuthToken - * @throws \Exception - */ - public function new_access_token(OAuthToken $token, OAuthConsumer $consumer, $verifier = null) - { - Logger::log(__function__ . ":" . $token . ", " . $consumer . ", " . $verifier); - - // return a new access token attached to this consumer - // for the user associated with this token if the request token - // is authorized - // should also invalidate the request token - - $ret = null; - - // get user for this verifier - $uverifier = DI::config()->get("oauth", $verifier); - Logger::log(__function__ . ":" . $verifier . "," . $uverifier); - - if (is_null($verifier) || ($uverifier !== false)) { - $key = self::genToken(); - $sec = self::genToken(); - $r = DBA::insert( - 'tokens', - [ - 'id' => $key, - 'secret' => $sec, - 'client_id' => $consumer->key, - 'scope' => 'access', - 'expires' => time() + ACCESS_TOKEN_DURATION, - 'uid' => $uverifier - ] - ); - - if ($r) { - $ret = new OAuthToken($key, $sec); - } - } - - DBA::delete('tokens', ['id' => $token->key]); - - if (!is_null($ret) && !is_null($uverifier)) { - DI::config()->delete("oauth", $verifier); - } - - return $ret; - } -} diff --git a/src/Object/Thread.php b/src/Object/Thread.php index f62b14c71e..6b31ad7049 100644 --- a/src/Object/Thread.php +++ b/src/Object/Thread.php @@ -25,7 +25,7 @@ use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\DI; use Friendica\Protocol\Activity; -use Friendica\Util\Security; +use Friendica\Security\Security; /** * A list of threads diff --git a/src/Security/ExAuth.php b/src/Security/ExAuth.php new file mode 100644 index 0000000000..87f236d4ec --- /dev/null +++ b/src/Security/ExAuth.php @@ -0,0 +1,391 @@ + + * modified for Friendica by Michael Vogel + * published under GPL + * + * Latest version of the original script for joomla is available at: + * http://87.230.15.86/~dado/ejabberd/joomla-login + * + * Installation: + * + * - Change it's owner to whichever user is running the server, ie. ejabberd + * $ chown ejabberd:ejabberd /path/to/friendica/bin/auth_ejabberd.php + * + * - Change the access mode so it is readable only to the user ejabberd and has exec + * $ chmod 700 /path/to/friendica/bin/auth_ejabberd.php + * + * - Edit your ejabberd.cfg file, comment out your auth_method and add: + * {auth_method, external}. + * {extauth_program, "/path/to/friendica/bin/auth_ejabberd.php"}. + * + * - Restart your ejabberd service, you should be able to login with your friendica auth info + * + * Other hints: + * - if your users have a space or a @ in their nickname, they'll run into trouble + * registering with any client so they should be instructed to replace these chars + * " " (space) is replaced with "%20" + * "@" is replaced with "(a)" + * + */ + +namespace Friendica\Security; + +use Exception; +use Friendica\App; +use Friendica\Core\Config\IConfig; +use Friendica\Core\PConfig\IPConfig; +use Friendica\Database\Database; +use Friendica\DI; +use Friendica\Model\User; +use Friendica\Network\HTTPException; +use Friendica\Util\PidFile; + +class ExAuth +{ + private $bDebug; + private $host; + + /** + * @var App\Mode + */ + private $appMode; + /** + * @var IConfig + */ + private $config; + /** + * @var IPConfig + */ + private $pConfig; + /** + * @var Database + */ + private $dba; + /** + * @var App\BaseURL + */ + private $baseURL; + + /** + * @param App\Mode $appMode + * @param IConfig $config + * @param IPConfig $pConfig + * @param Database $dba + * @param App\BaseURL $baseURL + * @throws Exception + */ + public function __construct(App\Mode $appMode, IConfig $config, IPConfig $pConfig, Database $dba, App\BaseURL $baseURL) + { + $this->appMode = $appMode; + $this->config = $config; + $this->pConfig = $pConfig; + $this->dba = $dba; + $this->baseURL = $baseURL; + + $this->bDebug = (int)$config->get('jabber', 'debug'); + + openlog('auth_ejabberd', LOG_PID, LOG_USER); + + $this->writeLog(LOG_NOTICE, 'start'); + } + + /** + * Standard input reading function, executes the auth with the provided + * parameters + * + * @throws HTTPException\InternalServerErrorException + */ + public function readStdin() + { + if (!$this->appMode->isNormal()) { + $this->writeLog(LOG_ERR, 'The node isn\'t ready.'); + return; + } + + while (!feof(STDIN)) { + // Quit if the database connection went down + if (!$this->dba->isConnected()) { + $this->writeLog(LOG_ERR, 'the database connection went down'); + return; + } + + $iHeader = fgets(STDIN, 3); + if (empty($iHeader)) { + $this->writeLog(LOG_ERR, 'empty stdin'); + return; + } + + $aLength = unpack('n', $iHeader); + $iLength = $aLength['1']; + + // No data? Then quit + if ($iLength == 0) { + $this->writeLog(LOG_ERR, 'we got no data, quitting'); + return; + } + + // Fetching the data + $sData = fgets(STDIN, $iLength + 1); + $this->writeLog(LOG_DEBUG, 'received data: ' . $sData); + $aCommand = explode(':', $sData); + if (is_array($aCommand)) { + switch ($aCommand[0]) { + case 'isuser': + // Check the existance of a given username + $this->isUser($aCommand); + break; + case 'auth': + // Check if the givven password is correct + $this->auth($aCommand); + break; + case 'setpass': + // We don't accept the setting of passwords here + $this->writeLog(LOG_NOTICE, 'setpass command disabled'); + fwrite(STDOUT, pack('nn', 2, 0)); + break; + default: + // We don't know the given command + $this->writeLog(LOG_NOTICE, 'unknown command ' . $aCommand[0]); + fwrite(STDOUT, pack('nn', 2, 0)); + break; + } + } else { + $this->writeLog(LOG_NOTICE, 'invalid command string ' . $sData); + fwrite(STDOUT, pack('nn', 2, 0)); + } + } + } + + /** + * Check if the given username exists + * + * @param array $aCommand The command array + * @throws HTTPException\InternalServerErrorException + */ + private function isUser(array $aCommand) + { + // Check if there is a username + if (!isset($aCommand[1])) { + $this->writeLog(LOG_NOTICE, 'invalid isuser command, no username given'); + fwrite(STDOUT, pack('nn', 2, 0)); + return; + } + + // We only allow one process per hostname. So we set a lock file + // Problem: We get the firstname after the first auth - not before + $this->setHost($aCommand[2]); + + // Now we check if the given user is valid + $sUser = str_replace(['%20', '(a)'], [' ', '@'], $aCommand[1]); + + // Does the hostname match? So we try directly + if ($this->baseURL->getHostname() == $aCommand[2]) { + $this->writeLog(LOG_INFO, 'internal user check for ' . $sUser . '@' . $aCommand[2]); + $found = $this->dba->exists('user', ['nickname' => $sUser]); + } else { + $found = false; + } + + // If the hostnames doesn't match or there is some failure, we try to check remotely + if (!$found) { + $found = $this->checkUser($aCommand[2], $aCommand[1], true); + } + + if ($found) { + // The user is okay + $this->writeLog(LOG_NOTICE, 'valid user: ' . $sUser); + fwrite(STDOUT, pack('nn', 2, 1)); + } else { + // The user isn't okay + $this->writeLog(LOG_WARNING, 'invalid user: ' . $sUser); + fwrite(STDOUT, pack('nn', 2, 0)); + } + } + + /** + * Check remote user existance via HTTP(S) + * + * @param string $host The hostname + * @param string $user Username + * @param boolean $ssl Should the check be done via SSL? + * + * @return boolean Was the user found? + * @throws HTTPException\InternalServerErrorException + */ + private function checkUser($host, $user, $ssl) + { + $this->writeLog(LOG_INFO, 'external user check for ' . $user . '@' . $host); + + $url = ($ssl ? 'https' : 'http') . '://' . $host . '/noscrape/' . $user; + + $curlResult = DI::httpRequest()->get($url); + + if (!$curlResult->isSuccess()) { + return false; + } + + if ($curlResult->getReturnCode() != 200) { + return false; + } + + $json = @json_decode($curlResult->getBody()); + if (!is_object($json)) { + return false; + } + + return $json->nick == $user; + } + + /** + * Authenticate the given user and password + * + * @param array $aCommand The command array + * @throws Exception + */ + private function auth(array $aCommand) + { + // check user authentication + if (sizeof($aCommand) != 4) { + $this->writeLog(LOG_NOTICE, 'invalid auth command, data missing'); + fwrite(STDOUT, pack('nn', 2, 0)); + return; + } + + // We only allow one process per hostname. So we set a lock file + // Problem: We get the firstname after the first auth - not before + $this->setHost($aCommand[2]); + + // We now check if the password match + $sUser = str_replace(['%20', '(a)'], [' ', '@'], $aCommand[1]); + + $Error = false; + // Does the hostname match? So we try directly + if ($this->baseURL->getHostname() == $aCommand[2]) { + try { + $this->writeLog(LOG_INFO, 'internal auth for ' . $sUser . '@' . $aCommand[2]); + User::getIdFromPasswordAuthentication($sUser, $aCommand[3], true); + } catch (HTTPException\ForbiddenException $ex) { + // User exists, authentication failed + $this->writeLog(LOG_INFO, 'check against alternate password for ' . $sUser . '@' . $aCommand[2]); + $aUser = User::getByNickname($sUser, ['uid']); + $sPassword = $this->pConfig->get($aUser['uid'], 'xmpp', 'password', null, true); + $Error = ($aCommand[3] != $sPassword); + } catch (\Throwable $ex) { + // User doesn't exist and any other failure case + $this->writeLog(LOG_WARNING, $ex->getMessage() . ': ' . $sUser); + $Error = true; + } + } else { + $Error = true; + } + + // If the hostnames doesn't match or there is some failure, we try to check remotely + if ($Error && !$this->checkCredentials($aCommand[2], $aCommand[1], $aCommand[3], true)) { + $this->writeLog(LOG_WARNING, 'authentification failed for user ' . $sUser . '@' . $aCommand[2]); + fwrite(STDOUT, pack('nn', 2, 0)); + } else { + $this->writeLog(LOG_NOTICE, 'authentificated user ' . $sUser . '@' . $aCommand[2]); + fwrite(STDOUT, pack('nn', 2, 1)); + } + } + + /** + * Check remote credentials via HTTP(S) + * + * @param string $host The hostname + * @param string $user Username + * @param string $password Password + * @param boolean $ssl Should the check be done via SSL? + * + * @return boolean Are the credentials okay? + */ + private function checkCredentials($host, $user, $password, $ssl) + { + $this->writeLog(LOG_INFO, 'external credential check for ' . $user . '@' . $host); + + $url = ($ssl ? 'https' : 'http') . '://' . $host . '/api/account/verify_credentials.json?skip_status=true'; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_NOBODY, true); + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($ch, CURLOPT_USERPWD, $user . ':' . $password); + + curl_exec($ch); + $curl_info = @curl_getinfo($ch); + $http_code = $curl_info['http_code']; + curl_close($ch); + + $this->writeLog(LOG_INFO, 'external auth for ' . $user . '@' . $host . ' returned ' . $http_code); + + return $http_code == 200; + } + + /** + * Set the hostname for this process + * + * @param string $host The hostname + */ + private function setHost($host) + { + if (!empty($this->host)) { + return; + } + + $this->writeLog(LOG_INFO, 'Hostname for process ' . getmypid() . ' is ' . $host); + + $this->host = $host; + + $lockpath = $this->config->get('jabber', 'lockpath'); + if (is_null($lockpath)) { + $this->writeLog(LOG_INFO, 'No lockpath defined.'); + return; + } + + $file = $lockpath . DIRECTORY_SEPARATOR . $host; + if (PidFile::isRunningProcess($file)) { + if (PidFile::killProcess($file)) { + $this->writeLog(LOG_INFO, 'Old process was successfully killed'); + } else { + $this->writeLog(LOG_ERR, "The old Process wasn't killed in time. We now quit our process."); + die(); + } + } + + // Now it is safe to create the pid file + PidFile::create($file); + if (!file_exists($file)) { + $this->writeLog(LOG_WARNING, 'Logfile ' . $file . " couldn't be created."); + } + } + + /** + * write data to the syslog + * + * @param integer $loglevel The syslog loglevel + * @param string $sMessage The syslog message + */ + private function writeLog($loglevel, $sMessage) + { + if (!$this->bDebug && ($loglevel >= LOG_DEBUG)) { + return; + } + syslog($loglevel, $sMessage); + } + + /** + * destroy the class, close the syslog connection. + */ + public function __destruct() + { + $this->writeLog(LOG_NOTICE, 'stop'); + closelog(); + } +} diff --git a/src/Security/FKOAuth1.php b/src/Security/FKOAuth1.php new file mode 100644 index 0000000000..48f8a54b16 --- /dev/null +++ b/src/Security/FKOAuth1.php @@ -0,0 +1,67 @@ +. + * + */ + +namespace Friendica\Network; + +use Friendica\Core\Logger; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Security\FKOAuthDataStore; +use OAuthServer; +use OAuthSignatureMethod_HMAC_SHA1; +use OAuthSignatureMethod_PLAINTEXT; + +/** + * OAuth protocol + */ +class FKOAuth1 extends OAuthServer +{ + /** + * Constructor + */ + public function __construct() + { + parent::__construct(new FKOAuthDataStore()); + $this->add_signature_method(new OAuthSignatureMethod_PLAINTEXT()); + $this->add_signature_method(new OAuthSignatureMethod_HMAC_SHA1()); + } + + /** + * @param string $uid user id + * @return void + * @throws HTTPException\ForbiddenException + * @throws HTTPException\InternalServerErrorException + */ + public function loginUser($uid) + { + Logger::notice("FKOAuth1::loginUser $uid"); + $a = DI::app(); + $record = DBA::selectFirst('user', [], ['uid' => $uid, 'blocked' => 0, 'account_expired' => 0, 'account_removed' => 0, 'verified' => 1]); + + if (!DBA::isResult($record)) { + Logger::info('FKOAuth1::loginUser failure', ['server' => $_SERVER]); + header('HTTP/1.0 401 Unauthorized'); + die('This api requires login'); + } + + DI::auth()->setForUser($a, $record, true); + } +} diff --git a/src/Security/FKOAuthDataStore.php b/src/Security/FKOAuthDataStore.php new file mode 100644 index 0000000000..d9c6895efb --- /dev/null +++ b/src/Security/FKOAuthDataStore.php @@ -0,0 +1,197 @@ +. + * + */ + +namespace Friendica\Security; + +use Friendica\Core\Logger; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Util\Strings; +use OAuthConsumer; +use OAuthDataStore; +use OAuthToken; + +define('REQUEST_TOKEN_DURATION', 300); +define('ACCESS_TOKEN_DURATION', 31536000); + +/** + * OAuthDataStore class + */ +class FKOAuthDataStore extends OAuthDataStore +{ + /** + * @return string + * @throws \Exception + */ + private static function genToken() + { + return Strings::getRandomHex(32); + } + + /** + * @param string $consumer_key key + * @return OAuthConsumer|null + * @throws \Exception + */ + public function lookup_consumer($consumer_key) + { + Logger::log(__function__ . ":" . $consumer_key); + + $s = DBA::select('clients', ['client_id', 'pw', 'redirect_uri'], ['client_id' => $consumer_key]); + $r = DBA::toArray($s); + + if (DBA::isResult($r)) { + return new OAuthConsumer($r[0]['client_id'], $r[0]['pw'], $r[0]['redirect_uri']); + } + + return null; + } + + /** + * @param OAuthConsumer $consumer + * @param string $token_type + * @param string $token_id + * @return OAuthToken|null + * @throws \Exception + */ + public function lookup_token(OAuthConsumer $consumer, $token_type, $token_id) + { + Logger::log(__function__ . ":" . $consumer . ", " . $token_type . ", " . $token_id); + + $s = DBA::select('tokens', ['id', 'secret', 'scope', 'expires', 'uid'], ['client_id' => $consumer->key, 'scope' => $token_type, 'id' => $token_id]); + $r = DBA::toArray($s); + + if (DBA::isResult($r)) { + $ot = new OAuthToken($r[0]['id'], $r[0]['secret']); + $ot->scope = $r[0]['scope']; + $ot->expires = $r[0]['expires']; + $ot->uid = $r[0]['uid']; + return $ot; + } + + return null; + } + + /** + * @param OAuthConsumer $consumer + * @param OAuthToken $token + * @param string $nonce + * @param int $timestamp + * @return mixed + * @throws \Exception + */ + public function lookup_nonce(OAuthConsumer $consumer, OAuthToken $token, $nonce, int $timestamp) + { + $token = DBA::selectFirst('tokens', ['id', 'secret'], ['client_id' => $consumer->key, 'id' => $nonce, 'expires' => $timestamp]); + if (DBA::isResult($token)) { + return new OAuthToken($token['id'], $token['secret']); + } + + return null; + } + + /** + * @param OAuthConsumer $consumer + * @param string $callback + * @return OAuthToken|null + * @throws \Exception + */ + public function new_request_token(OAuthConsumer $consumer, $callback = null) + { + Logger::log(__function__ . ":" . $consumer . ", " . $callback); + $key = self::genToken(); + $sec = self::genToken(); + + if ($consumer->key) { + $k = $consumer->key; + } else { + $k = $consumer; + } + + $r = DBA::insert( + 'tokens', + [ + 'id' => $key, + 'secret' => $sec, + 'client_id' => $k, + 'scope' => 'request', + 'expires' => time() + REQUEST_TOKEN_DURATION + ] + ); + + if (!$r) { + return null; + } + + return new OAuthToken($key, $sec); + } + + /** + * @param OAuthToken $token token + * @param OAuthConsumer $consumer consumer + * @param string $verifier optional, defult null + * @return OAuthToken + * @throws \Exception + */ + public function new_access_token(OAuthToken $token, OAuthConsumer $consumer, $verifier = null) + { + Logger::log(__function__ . ":" . $token . ", " . $consumer . ", " . $verifier); + + // return a new access token attached to this consumer + // for the user associated with this token if the request token + // is authorized + // should also invalidate the request token + + $ret = null; + + // get user for this verifier + $uverifier = DI::config()->get("oauth", $verifier); + Logger::log(__function__ . ":" . $verifier . "," . $uverifier); + + if (is_null($verifier) || ($uverifier !== false)) { + $key = self::genToken(); + $sec = self::genToken(); + $r = DBA::insert( + 'tokens', + [ + 'id' => $key, + 'secret' => $sec, + 'client_id' => $consumer->key, + 'scope' => 'access', + 'expires' => time() + ACCESS_TOKEN_DURATION, + 'uid' => $uverifier + ] + ); + + if ($r) { + $ret = new OAuthToken($key, $sec); + } + } + + DBA::delete('tokens', ['id' => $token->key]); + + if (!is_null($ret) && !is_null($uverifier)) { + DI::config()->delete("oauth", $verifier); + } + + return $ret; + } +} diff --git a/src/Security/Security.php b/src/Security/Security.php new file mode 100644 index 0000000000..a75b9168b5 --- /dev/null +++ b/src/Security/Security.php @@ -0,0 +1,148 @@ +. + * + */ + +namespace Friendica\Security; + +use Friendica\Database\DBA; +use Friendica\Model\Contact; +use Friendica\Model\Group; +use Friendica\Model\User; +use Friendica\Core\Session; + +/** + * Secures that User is allow to do requests + */ +class Security +{ + public static function canWriteToUserWall($owner) + { + static $verified = 0; + + if (!Session::isAuthenticated()) { + return false; + } + + $uid = local_user(); + if ($uid == $owner) { + return true; + } + + if (local_user() && ($owner == 0)) { + return true; + } + + if (!empty(Session::getRemoteContactID($owner))) { + // use remembered decision and avoid a DB lookup for each and every display item + // DO NOT use this function if there are going to be multiple owners + // We have a contact-id for an authenticated remote user, this block determines if the contact + // belongs to this page owner, and has the necessary permissions to post content + + if ($verified === 2) { + return true; + } elseif ($verified === 1) { + return false; + } else { + $cid = Session::getRemoteContactID($owner); + if (!$cid) { + return false; + } + + $r = q("SELECT `contact`.*, `user`.`page-flags` FROM `contact` INNER JOIN `user` on `user`.`uid` = `contact`.`uid` + WHERE `contact`.`uid` = %d AND `contact`.`id` = %d AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0 + AND `user`.`blockwall` = 0 AND `readonly` = 0 AND (`contact`.`rel` IN (%d , %d) OR `user`.`page-flags` = %d) LIMIT 1", + intval($owner), + intval($cid), + intval(Contact::SHARING), + intval(Contact::FRIEND), + intval(User::PAGE_FLAGS_COMMUNITY) + ); + + if (DBA::isResult($r)) { + $verified = 2; + return true; + } else { + $verified = 1; + } + } + } + + return false; + } + + /** + * Create a permission string for an element based on the visitor + * + * @param integer $owner_id User ID of the owner of the element + * @param boolean $accessible Should the element be accessible anyway? + * @return string SQL permissions + */ + public static function getPermissionsSQLByUserId(int $owner_id, bool $accessible = false) + { + $local_user = local_user(); + $remote_contact = Session::getRemoteContactID($owner_id); + $acc_sql = ''; + + if ($accessible) { + $acc_sql = ' OR `accessible`'; + } + + /* + * Construct permissions + * + * default permissions - anonymous user + */ + $sql = " AND (allow_cid = '' + AND allow_gid = '' + AND deny_cid = '' + AND deny_gid = ''" . $acc_sql . ") "; + + /* + * Profile owner - everything is visible + */ + if ($local_user && $local_user == $owner_id) { + $sql = ''; + /* + * Authenticated visitor. Load the groups the visitor belongs to. + */ + } elseif ($remote_contact) { + $gs = '<<>>'; // should be impossible to match + + $groups = Group::getIdsByContactId($remote_contact); + + if (is_array($groups)) { + foreach ($groups as $g) { + $gs .= '|<' . intval($g) . '>'; + } + } + + $sql = sprintf( + " AND (NOT (deny_cid REGEXP '<%d>' OR deny_gid REGEXP '%s') + AND (allow_cid REGEXP '<%d>' OR allow_gid REGEXP '%s' + OR (allow_cid = '' AND allow_gid = ''))" . $acc_sql . ") ", + intval($remote_contact), + DBA::escape($gs), + intval($remote_contact), + DBA::escape($gs) + ); + } + return $sql; + } +} diff --git a/src/Util/ExAuth.php b/src/Util/ExAuth.php deleted file mode 100644 index 7771712f31..0000000000 --- a/src/Util/ExAuth.php +++ /dev/null @@ -1,390 +0,0 @@ - - * modified for Friendica by Michael Vogel - * published under GPL - * - * Latest version of the original script for joomla is available at: - * http://87.230.15.86/~dado/ejabberd/joomla-login - * - * Installation: - * - * - Change it's owner to whichever user is running the server, ie. ejabberd - * $ chown ejabberd:ejabberd /path/to/friendica/bin/auth_ejabberd.php - * - * - Change the access mode so it is readable only to the user ejabberd and has exec - * $ chmod 700 /path/to/friendica/bin/auth_ejabberd.php - * - * - Edit your ejabberd.cfg file, comment out your auth_method and add: - * {auth_method, external}. - * {extauth_program, "/path/to/friendica/bin/auth_ejabberd.php"}. - * - * - Restart your ejabberd service, you should be able to login with your friendica auth info - * - * Other hints: - * - if your users have a space or a @ in their nickname, they'll run into trouble - * registering with any client so they should be instructed to replace these chars - * " " (space) is replaced with "%20" - * "@" is replaced with "(a)" - * - */ - -namespace Friendica\Util; - -use Exception; -use Friendica\App; -use Friendica\Core\Config\IConfig; -use Friendica\Core\PConfig\IPConfig; -use Friendica\Database\Database; -use Friendica\DI; -use Friendica\Model\User; -use Friendica\Network\HTTPException; - -class ExAuth -{ - private $bDebug; - private $host; - - /** - * @var App\Mode - */ - private $appMode; - /** - * @var IConfig - */ - private $config; - /** - * @var IPConfig - */ - private $pConfig; - /** - * @var Database - */ - private $dba; - /** - * @var App\BaseURL - */ - private $baseURL; - - /** - * @param App\Mode $appMode - * @param IConfig $config - * @param IPConfig $pConfig - * @param Database $dba - * @param App\BaseURL $baseURL - * @throws Exception - */ - public function __construct(App\Mode $appMode, IConfig $config, IPConfig $pConfig, Database $dba, App\BaseURL $baseURL) - { - $this->appMode = $appMode; - $this->config = $config; - $this->pConfig = $pConfig; - $this->dba = $dba; - $this->baseURL = $baseURL; - - $this->bDebug = (int)$config->get('jabber', 'debug'); - - openlog('auth_ejabberd', LOG_PID, LOG_USER); - - $this->writeLog(LOG_NOTICE, 'start'); - } - - /** - * Standard input reading function, executes the auth with the provided - * parameters - * - * @throws HTTPException\InternalServerErrorException - */ - public function readStdin() - { - if (!$this->appMode->isNormal()) { - $this->writeLog(LOG_ERR, 'The node isn\'t ready.'); - return; - } - - while (!feof(STDIN)) { - // Quit if the database connection went down - if (!$this->dba->isConnected()) { - $this->writeLog(LOG_ERR, 'the database connection went down'); - return; - } - - $iHeader = fgets(STDIN, 3); - if (empty($iHeader)) { - $this->writeLog(LOG_ERR, 'empty stdin'); - return; - } - - $aLength = unpack('n', $iHeader); - $iLength = $aLength['1']; - - // No data? Then quit - if ($iLength == 0) { - $this->writeLog(LOG_ERR, 'we got no data, quitting'); - return; - } - - // Fetching the data - $sData = fgets(STDIN, $iLength + 1); - $this->writeLog(LOG_DEBUG, 'received data: ' . $sData); - $aCommand = explode(':', $sData); - if (is_array($aCommand)) { - switch ($aCommand[0]) { - case 'isuser': - // Check the existance of a given username - $this->isUser($aCommand); - break; - case 'auth': - // Check if the givven password is correct - $this->auth($aCommand); - break; - case 'setpass': - // We don't accept the setting of passwords here - $this->writeLog(LOG_NOTICE, 'setpass command disabled'); - fwrite(STDOUT, pack('nn', 2, 0)); - break; - default: - // We don't know the given command - $this->writeLog(LOG_NOTICE, 'unknown command ' . $aCommand[0]); - fwrite(STDOUT, pack('nn', 2, 0)); - break; - } - } else { - $this->writeLog(LOG_NOTICE, 'invalid command string ' . $sData); - fwrite(STDOUT, pack('nn', 2, 0)); - } - } - } - - /** - * Check if the given username exists - * - * @param array $aCommand The command array - * @throws HTTPException\InternalServerErrorException - */ - private function isUser(array $aCommand) - { - // Check if there is a username - if (!isset($aCommand[1])) { - $this->writeLog(LOG_NOTICE, 'invalid isuser command, no username given'); - fwrite(STDOUT, pack('nn', 2, 0)); - return; - } - - // We only allow one process per hostname. So we set a lock file - // Problem: We get the firstname after the first auth - not before - $this->setHost($aCommand[2]); - - // Now we check if the given user is valid - $sUser = str_replace(['%20', '(a)'], [' ', '@'], $aCommand[1]); - - // Does the hostname match? So we try directly - if ($this->baseURL->getHostname() == $aCommand[2]) { - $this->writeLog(LOG_INFO, 'internal user check for ' . $sUser . '@' . $aCommand[2]); - $found = $this->dba->exists('user', ['nickname' => $sUser]); - } else { - $found = false; - } - - // If the hostnames doesn't match or there is some failure, we try to check remotely - if (!$found) { - $found = $this->checkUser($aCommand[2], $aCommand[1], true); - } - - if ($found) { - // The user is okay - $this->writeLog(LOG_NOTICE, 'valid user: ' . $sUser); - fwrite(STDOUT, pack('nn', 2, 1)); - } else { - // The user isn't okay - $this->writeLog(LOG_WARNING, 'invalid user: ' . $sUser); - fwrite(STDOUT, pack('nn', 2, 0)); - } - } - - /** - * Check remote user existance via HTTP(S) - * - * @param string $host The hostname - * @param string $user Username - * @param boolean $ssl Should the check be done via SSL? - * - * @return boolean Was the user found? - * @throws HTTPException\InternalServerErrorException - */ - private function checkUser($host, $user, $ssl) - { - $this->writeLog(LOG_INFO, 'external user check for ' . $user . '@' . $host); - - $url = ($ssl ? 'https' : 'http') . '://' . $host . '/noscrape/' . $user; - - $curlResult = DI::httpRequest()->get($url); - - if (!$curlResult->isSuccess()) { - return false; - } - - if ($curlResult->getReturnCode() != 200) { - return false; - } - - $json = @json_decode($curlResult->getBody()); - if (!is_object($json)) { - return false; - } - - return $json->nick == $user; - } - - /** - * Authenticate the given user and password - * - * @param array $aCommand The command array - * @throws Exception - */ - private function auth(array $aCommand) - { - // check user authentication - if (sizeof($aCommand) != 4) { - $this->writeLog(LOG_NOTICE, 'invalid auth command, data missing'); - fwrite(STDOUT, pack('nn', 2, 0)); - return; - } - - // We only allow one process per hostname. So we set a lock file - // Problem: We get the firstname after the first auth - not before - $this->setHost($aCommand[2]); - - // We now check if the password match - $sUser = str_replace(['%20', '(a)'], [' ', '@'], $aCommand[1]); - - $Error = false; - // Does the hostname match? So we try directly - if ($this->baseURL->getHostname() == $aCommand[2]) { - try { - $this->writeLog(LOG_INFO, 'internal auth for ' . $sUser . '@' . $aCommand[2]); - User::getIdFromPasswordAuthentication($sUser, $aCommand[3], true); - } catch (HTTPException\ForbiddenException $ex) { - // User exists, authentication failed - $this->writeLog(LOG_INFO, 'check against alternate password for ' . $sUser . '@' . $aCommand[2]); - $aUser = User::getByNickname($sUser, ['uid']); - $sPassword = $this->pConfig->get($aUser['uid'], 'xmpp', 'password', null, true); - $Error = ($aCommand[3] != $sPassword); - } catch (\Throwable $ex) { - // User doesn't exist and any other failure case - $this->writeLog(LOG_WARNING, $ex->getMessage() . ': ' . $sUser); - $Error = true; - } - } else { - $Error = true; - } - - // If the hostnames doesn't match or there is some failure, we try to check remotely - if ($Error && !$this->checkCredentials($aCommand[2], $aCommand[1], $aCommand[3], true)) { - $this->writeLog(LOG_WARNING, 'authentification failed for user ' . $sUser . '@' . $aCommand[2]); - fwrite(STDOUT, pack('nn', 2, 0)); - } else { - $this->writeLog(LOG_NOTICE, 'authentificated user ' . $sUser . '@' . $aCommand[2]); - fwrite(STDOUT, pack('nn', 2, 1)); - } - } - - /** - * Check remote credentials via HTTP(S) - * - * @param string $host The hostname - * @param string $user Username - * @param string $password Password - * @param boolean $ssl Should the check be done via SSL? - * - * @return boolean Are the credentials okay? - */ - private function checkCredentials($host, $user, $password, $ssl) - { - $this->writeLog(LOG_INFO, 'external credential check for ' . $user . '@' . $host); - - $url = ($ssl ? 'https' : 'http') . '://' . $host . '/api/account/verify_credentials.json?skip_status=true'; - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); - curl_setopt($ch, CURLOPT_HEADER, true); - curl_setopt($ch, CURLOPT_NOBODY, true); - curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); - curl_setopt($ch, CURLOPT_USERPWD, $user . ':' . $password); - - curl_exec($ch); - $curl_info = @curl_getinfo($ch); - $http_code = $curl_info['http_code']; - curl_close($ch); - - $this->writeLog(LOG_INFO, 'external auth for ' . $user . '@' . $host . ' returned ' . $http_code); - - return $http_code == 200; - } - - /** - * Set the hostname for this process - * - * @param string $host The hostname - */ - private function setHost($host) - { - if (!empty($this->host)) { - return; - } - - $this->writeLog(LOG_INFO, 'Hostname for process ' . getmypid() . ' is ' . $host); - - $this->host = $host; - - $lockpath = $this->config->get('jabber', 'lockpath'); - if (is_null($lockpath)) { - $this->writeLog(LOG_INFO, 'No lockpath defined.'); - return; - } - - $file = $lockpath . DIRECTORY_SEPARATOR . $host; - if (PidFile::isRunningProcess($file)) { - if (PidFile::killProcess($file)) { - $this->writeLog(LOG_INFO, 'Old process was successfully killed'); - } else { - $this->writeLog(LOG_ERR, "The old Process wasn't killed in time. We now quit our process."); - die(); - } - } - - // Now it is safe to create the pid file - PidFile::create($file); - if (!file_exists($file)) { - $this->writeLog(LOG_WARNING, 'Logfile ' . $file . " couldn't be created."); - } - } - - /** - * write data to the syslog - * - * @param integer $loglevel The syslog loglevel - * @param string $sMessage The syslog message - */ - private function writeLog($loglevel, $sMessage) - { - if (!$this->bDebug && ($loglevel >= LOG_DEBUG)) { - return; - } - syslog($loglevel, $sMessage); - } - - /** - * destroy the class, close the syslog connection. - */ - public function __destruct() - { - $this->writeLog(LOG_NOTICE, 'stop'); - closelog(); - } -} diff --git a/src/Util/Security.php b/src/Util/Security.php deleted file mode 100644 index 4233382160..0000000000 --- a/src/Util/Security.php +++ /dev/null @@ -1,148 +0,0 @@ -. - * - */ - -namespace Friendica\Util; - -use Friendica\Database\DBA; -use Friendica\Model\Contact; -use Friendica\Model\Group; -use Friendica\Model\User; -use Friendica\Core\Session; - -/** - * Secures that User is allow to do requests - */ -class Security -{ - public static function canWriteToUserWall($owner) - { - static $verified = 0; - - if (!Session::isAuthenticated()) { - return false; - } - - $uid = local_user(); - if ($uid == $owner) { - return true; - } - - if (local_user() && ($owner == 0)) { - return true; - } - - if (!empty(Session::getRemoteContactID($owner))) { - // use remembered decision and avoid a DB lookup for each and every display item - // DO NOT use this function if there are going to be multiple owners - // We have a contact-id for an authenticated remote user, this block determines if the contact - // belongs to this page owner, and has the necessary permissions to post content - - if ($verified === 2) { - return true; - } elseif ($verified === 1) { - return false; - } else { - $cid = Session::getRemoteContactID($owner); - if (!$cid) { - return false; - } - - $r = q("SELECT `contact`.*, `user`.`page-flags` FROM `contact` INNER JOIN `user` on `user`.`uid` = `contact`.`uid` - WHERE `contact`.`uid` = %d AND `contact`.`id` = %d AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0 - AND `user`.`blockwall` = 0 AND `readonly` = 0 AND (`contact`.`rel` IN (%d , %d) OR `user`.`page-flags` = %d) LIMIT 1", - intval($owner), - intval($cid), - intval(Contact::SHARING), - intval(Contact::FRIEND), - intval(User::PAGE_FLAGS_COMMUNITY) - ); - - if (DBA::isResult($r)) { - $verified = 2; - return true; - } else { - $verified = 1; - } - } - } - - return false; - } - - /** - * Create a permission string for an element based on the visitor - * - * @param integer $owner_id User ID of the owner of the element - * @param boolean $accessible Should the element be accessible anyway? - * @return string SQL permissions - */ - public static function getPermissionsSQLByUserId(int $owner_id, bool $accessible = false) - { - $local_user = local_user(); - $remote_contact = Session::getRemoteContactID($owner_id); - $acc_sql = ''; - - if ($accessible) { - $acc_sql = ' OR `accessible`'; - } - - /* - * Construct permissions - * - * default permissions - anonymous user - */ - $sql = " AND (allow_cid = '' - AND allow_gid = '' - AND deny_cid = '' - AND deny_gid = ''" . $acc_sql . ") "; - - /* - * Profile owner - everything is visible - */ - if ($local_user && $local_user == $owner_id) { - $sql = ''; - /* - * Authenticated visitor. Load the groups the visitor belongs to. - */ - } elseif ($remote_contact) { - $gs = '<<>>'; // should be impossible to match - - $groups = Group::getIdsByContactId($remote_contact); - - if (is_array($groups)) { - foreach ($groups as $g) { - $gs .= '|<' . intval($g) . '>'; - } - } - - $sql = sprintf( - " AND (NOT (deny_cid REGEXP '<%d>' OR deny_gid REGEXP '%s') - AND (allow_cid REGEXP '<%d>' OR allow_gid REGEXP '%s' - OR (allow_cid = '' AND allow_gid = ''))" . $acc_sql . ") ", - intval($remote_contact), - DBA::escape($gs), - intval($remote_contact), - DBA::escape($gs) - ); - } - return $sql; - } -}