diff --git a/core/Api/ApiKey/Create.class.php b/core/Api/ApiKey/Create.class.php index bb2958e..c5da1dd 100644 --- a/core/Api/ApiKey/Create.class.php +++ b/core/Api/ApiKey/Create.class.php @@ -9,6 +9,7 @@ class Create extends Request { public function __construct($user, $externalCall = false) { parent::__construct($user, $externalCall, array()); $this->apiKeyAllowed = false; + $this->csrfTokenRequired = true; $this->loginRequired = true; } diff --git a/core/Api/ApiKey/Fetch.class.php b/core/Api/ApiKey/Fetch.class.php index c88be1a..df231f2 100644 --- a/core/Api/ApiKey/Fetch.class.php +++ b/core/Api/ApiKey/Fetch.class.php @@ -12,6 +12,7 @@ class Fetch extends Request { public function __construct($user, $externalCall = false) { parent::__construct($user, $externalCall, array()); $this->loginRequired = true; + $this->csrfTokenRequired = true; } public function execute($values = array()) { diff --git a/core/Api/ApiKey/Refresh.class.php b/core/Api/ApiKey/Refresh.class.php index 3def137..945c930 100644 --- a/core/Api/ApiKey/Refresh.class.php +++ b/core/Api/ApiKey/Refresh.class.php @@ -13,6 +13,7 @@ class Refresh extends Request { "id" => new Parameter("id", Parameter::TYPE_INT), )); $this->loginRequired = true; + $this->csrfTokenRequired = true; } private function apiKeyExists() { diff --git a/core/Api/ApiKey/Revoke.class.php b/core/Api/ApiKey/Revoke.class.php index 973a4cc..2cb15bb 100644 --- a/core/Api/ApiKey/Revoke.class.php +++ b/core/Api/ApiKey/Revoke.class.php @@ -13,6 +13,7 @@ class Revoke extends Request { "id" => new Parameter("id", Parameter::TYPE_INT), )); $this->loginRequired = true; + $this->csrfTokenRequired = true; } private function apiKeyExists() { diff --git a/core/Api/Notifications/Create.class.php b/core/Api/Notifications/Create.class.php index 5972570..3a84338 100644 --- a/core/Api/Notifications/Create.class.php +++ b/core/Api/Notifications/Create.class.php @@ -17,6 +17,7 @@ class Create extends Request { 'message' => new StringType('message', 256), )); $this->isPublic = false; + $this->csrfTokenRequired = true; } private function checkUser($userId) { diff --git a/core/Api/Notifications/Fetch.class.php b/core/Api/Notifications/Fetch.class.php index 5d3e342..f37dc60 100644 --- a/core/Api/Notifications/Fetch.class.php +++ b/core/Api/Notifications/Fetch.class.php @@ -12,6 +12,7 @@ class Fetch extends Request { public function __construct($user, $externalCall = false) { parent::__construct($user, $externalCall, array()); $this->loginRequired = true; + $this->csrfTokenRequired = true; } private function fetchUserNotifications() { diff --git a/core/Api/Request.class.php b/core/Api/Request.class.php index bc04cd6..49dfe77 100644 --- a/core/Api/Request.class.php +++ b/core/Api/Request.class.php @@ -17,6 +17,7 @@ class Request { protected bool $isDisabled; protected bool $apiKeyAllowed; protected int $requiredGroup; + protected bool $csrfTokenRequired; private array $aDefaultParams; private array $allowedMethods; @@ -37,6 +38,7 @@ class Request { $this->allowedMethods = array("GET", "POST"); $this->requiredGroup = 0; $this->lastError = ""; + $this->csrfTokenRequired = false; } protected function forbidMethod($method) { @@ -111,13 +113,13 @@ class Request { } if($this->loginRequired || $this->requiredGroup > 0) { - $authorized = false; + $apiKeyAuthorized = false; if(isset($values['api_key']) && $this->apiKeyAllowed) { $apiKey = $values['api_key']; - $authorized = $this->user->authorize($apiKey); + $apiKeyAuthorized = $this->user->authorize($apiKey); } - if(!$this->user->isLoggedIn() && !$authorized) { + if(!$this->user->isLoggedIn() && !$apiKeyAuthorized) { $this->lastError = 'You are not logged in.'; header('HTTP 1.1 401 Unauthorized'); return false; @@ -125,6 +127,14 @@ class Request { $this->lastError = "Insufficient permissions. Required group: ". GroupName($this->requiredGroup); header('HTTP 1.1 401 Unauthorized'); return false; + } else if($this->csrfTokenRequired && !$apiKeyAuthorized && $this->externalCall) { + // csrf token required + external call + // if it's not a call with API_KEY, check for csrf_token + if (!isset($values["csrf_token"]) || strcmp($values["csrf_token"], $this->user->getSession()->getCsrfToken()) !== 0) { + $this->lastError = "CSRF-Token mismatch"; + header('HTTP 1.1 403 Forbidden'); + return false; + } } } diff --git a/core/Api/SetLanguage.class.php b/core/Api/SetLanguage.class.php index 4f0c7e4..f956f0c 100644 --- a/core/Api/SetLanguage.class.php +++ b/core/Api/SetLanguage.class.php @@ -17,6 +17,7 @@ class SetLanguage extends Request { 'langId' => new Parameter('langId', Parameter::TYPE_INT, true, NULL), 'langCode' => new StringType('langCode', 5, true, NULL), )); + $this->csrfTokenRequired = true; } private function checkLanguage() { diff --git a/core/Api/User/Fetch.class.php b/core/Api/User/Fetch.class.php index 53e8962..99ca10a 100644 --- a/core/Api/User/Fetch.class.php +++ b/core/Api/User/Fetch.class.php @@ -20,6 +20,7 @@ class Fetch extends Request { $this->loginRequired = true; $this->requiredGroup = USER_GROUP_ADMIN; $this->userCount = 0; + $this->csrfTokenRequired = true; } private function getUserCount() { diff --git a/core/Api/User/Info.class.php b/core/Api/User/Info.class.php new file mode 100644 index 0000000..0ea8eba --- /dev/null +++ b/core/Api/User/Info.class.php @@ -0,0 +1,28 @@ +csrfTokenRequired = true; + } + + public function execute($values = array()) { + if(!parent::execute($values)) { + return false; + } + + if (!$this->user->isLoggedIn()) { + $this->result["loggedIn"] = false; + } else { + $this->result["loggedIn"] = true; + } + + $this->result["user"] = $this->user->jsonSerialize(); + return $this->success; + } +} \ No newline at end of file diff --git a/core/Api/User/Login.class.php b/core/Api/User/Login.class.php index c57508e..9fb3c8f 100644 --- a/core/Api/User/Login.class.php +++ b/core/Api/User/Login.class.php @@ -65,6 +65,7 @@ class Login extends Request { if(!($this->success = $this->user->createSession($uid, $stayLoggedIn))) { return $this->createError("Error creating Session: " . $sql->getLastError()); } else { + $this->result["loggedIn"] = true; $this->result['logoutIn'] = $this->user->getSession()->getExpiresSeconds(); $this->success = true; } diff --git a/core/Api/User/Logout.class.php b/core/Api/User/Logout.class.php index 365f6ea..a6f89b7 100644 --- a/core/Api/User/Logout.class.php +++ b/core/Api/User/Logout.class.php @@ -10,6 +10,7 @@ class Logout extends Request { parent::__construct($user, $externalCall); $this->loginRequired = true; $this->apiKeyAllowed = false; + $this->csrfTokenRequired = true; } public function execute($values = array()) { diff --git a/core/Configuration/CreateDatabase.class.php b/core/Configuration/CreateDatabase.class.php index 463ed8e..7d8b829 100755 --- a/core/Configuration/CreateDatabase.class.php +++ b/core/Configuration/CreateDatabase.class.php @@ -47,6 +47,7 @@ class CreateDatabase { ->addString("browser", 64) ->addJson("data", false, '{}') ->addBool("stay_logged_in", true) + ->addString("csrf_token", 16 ) ->primaryKey("uid", "user_id") ->foreignKey("user_id", "User", "uid", new CascadeStrategy()); diff --git a/core/Elements/View.class.php b/core/Elements/View.class.php index 25882da..45b2c90 100644 --- a/core/Elements/View.class.php +++ b/core/Elements/View.class.php @@ -21,7 +21,6 @@ abstract class View extends StaticView { } public function getTitle() { return $this->title; } - public function __toString() { return $this->getCode(); } public function getDocument() { return $this->document; } public function isSearchable() { return $this->searchable; } public function getReference() { return $this->reference; } diff --git a/core/Objects/Session.class.php b/core/Objects/Session.class.php index 4081293..cb0d749 100644 --- a/core/Objects/Session.class.php +++ b/core/Objects/Session.class.php @@ -19,15 +19,17 @@ class Session extends ApiObject { private ?string $os; private ?string $browser; private bool $stayLoggedIn; + private string $csrfToken; - public function __construct(User $user, ?int $sessionId) { + public function __construct(User $user, ?int $sessionId, ?string $csrfToken) { $this->user = $user; $this->sessionId = $sessionId; $this->stayLoggedIn = true; + $this->csrfToken = $csrfToken ?? generateRandomString(16); } public static function create($user, $stayLoggedIn) { - $session = new Session($user, null); + $session = new Session($user, null, null); if($session->insert($stayLoggedIn)) { return $session; } @@ -85,6 +87,7 @@ class Session extends ApiObject { 'ipAddress' => $this->ipAddress, 'os' => $this->os, 'browser' => $this->browser, + 'csrf_token' => $this->csrfToken ); } @@ -93,7 +96,7 @@ class Session extends ApiObject { $sql = $this->user->getSQL(); $minutes = Session::DURATION; - $columns = array("expires", "user_id", "ipAddress", "os", "browser", "data", "stay_logged_in"); + $columns = array("expires", "user_id", "ipAddress", "os", "browser", "data", "stay_logged_in", "csrf_token"); $success = $sql ->insert("Session", $columns) @@ -104,7 +107,8 @@ class Session extends ApiObject { $this->os, $this->browser, json_encode($_SESSION), - $stayLoggedIn) + $stayLoggedIn, + $this->csrfToken) ->returning("uid") ->execute(); @@ -135,8 +139,13 @@ class Session extends ApiObject { ->set("Session.os", $this->os) ->set("Session.browser", $this->browser) ->set("Session.data", json_encode($_SESSION)) + ->set("Session.csrf_token", $this->csrfToken) ->where(new Compare("Session.uid", $this->sessionId)) ->where(new Compare("Session.user_id", $this->user->getId())) ->execute(); } + + public function getCsrfToken(): string { + return $this->csrfToken; + } } diff --git a/core/Objects/User.class.php b/core/Objects/User.class.php index 28d6f56..7e3675f 100644 --- a/core/Objects/User.class.php +++ b/core/Objects/User.class.php @@ -71,12 +71,19 @@ class User extends ApiObject { } public function jsonSerialize() { - return array( - 'uid' => $this->uid, - 'name' => $this->username, - 'language' => $this->language, - 'session' => $this->session, - ); + if ($this->isLoggedIn()) { + return array( + 'uid' => $this->uid, + 'name' => $this->username, + 'groups' => $this->groups, + 'language' => $this->language->jsonSerialize(), + 'session' => $this->session->jsonSerialize(), + ); + } else { + return array( + 'language' => $this->language->jsonSerialize(), + ); + } } private function reset() { @@ -116,7 +123,7 @@ class User extends ApiObject { public function readData($userId, $sessionId, $sessionUpdate = true) { $res = $this->sql->select("User.name", "Language.uid as langId", "Language.code as langCode", "Language.name as langName", - "Session.data", "Session.stay_logged_in", "Group.uid as groupId", "Group.name as groupName") + "Session.data", "Session.stay_logged_in", "Session.csrf_token", "Group.uid as groupId", "Group.name as groupName") ->from("User") ->innerJoin("Session", "Session.user_id", "User.uid") ->leftJoin("Language", "User.language_id", "Language.uid") @@ -134,9 +141,10 @@ class User extends ApiObject { $success = false; } else { $row = $res[0]; + $csrfToken = $row["csrf_token"]; $this->username = $row['name']; $this->uid = $userId; - $this->session = new Session($this, $sessionId); + $this->session = new Session($this, $sessionId, $csrfToken); $this->session->setData(json_decode($row["data"] ?? '{}')); $this->session->stayLoggedIn($row["stay_logged_in"]); if($sessionUpdate) $this->session->update();