CSRF Token + small fixes

This commit is contained in:
Roman Hergenreder 2020-06-14 19:39:52 +02:00
parent 8fc0b4bb05
commit f87fdc83ae
17 changed files with 83 additions and 16 deletions

@ -9,6 +9,7 @@ class Create extends Request {
public function __construct($user, $externalCall = false) { public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array()); parent::__construct($user, $externalCall, array());
$this->apiKeyAllowed = false; $this->apiKeyAllowed = false;
$this->csrfTokenRequired = true;
$this->loginRequired = true; $this->loginRequired = true;
} }

@ -12,6 +12,7 @@ class Fetch extends Request {
public function __construct($user, $externalCall = false) { public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array()); parent::__construct($user, $externalCall, array());
$this->loginRequired = true; $this->loginRequired = true;
$this->csrfTokenRequired = true;
} }
public function execute($values = array()) { public function execute($values = array()) {

@ -13,6 +13,7 @@ class Refresh extends Request {
"id" => new Parameter("id", Parameter::TYPE_INT), "id" => new Parameter("id", Parameter::TYPE_INT),
)); ));
$this->loginRequired = true; $this->loginRequired = true;
$this->csrfTokenRequired = true;
} }
private function apiKeyExists() { private function apiKeyExists() {

@ -13,6 +13,7 @@ class Revoke extends Request {
"id" => new Parameter("id", Parameter::TYPE_INT), "id" => new Parameter("id", Parameter::TYPE_INT),
)); ));
$this->loginRequired = true; $this->loginRequired = true;
$this->csrfTokenRequired = true;
} }
private function apiKeyExists() { private function apiKeyExists() {

@ -17,6 +17,7 @@ class Create extends Request {
'message' => new StringType('message', 256), 'message' => new StringType('message', 256),
)); ));
$this->isPublic = false; $this->isPublic = false;
$this->csrfTokenRequired = true;
} }
private function checkUser($userId) { private function checkUser($userId) {

@ -12,6 +12,7 @@ class Fetch extends Request {
public function __construct($user, $externalCall = false) { public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array()); parent::__construct($user, $externalCall, array());
$this->loginRequired = true; $this->loginRequired = true;
$this->csrfTokenRequired = true;
} }
private function fetchUserNotifications() { private function fetchUserNotifications() {

@ -17,6 +17,7 @@ class Request {
protected bool $isDisabled; protected bool $isDisabled;
protected bool $apiKeyAllowed; protected bool $apiKeyAllowed;
protected int $requiredGroup; protected int $requiredGroup;
protected bool $csrfTokenRequired;
private array $aDefaultParams; private array $aDefaultParams;
private array $allowedMethods; private array $allowedMethods;
@ -37,6 +38,7 @@ class Request {
$this->allowedMethods = array("GET", "POST"); $this->allowedMethods = array("GET", "POST");
$this->requiredGroup = 0; $this->requiredGroup = 0;
$this->lastError = ""; $this->lastError = "";
$this->csrfTokenRequired = false;
} }
protected function forbidMethod($method) { protected function forbidMethod($method) {
@ -111,13 +113,13 @@ class Request {
} }
if($this->loginRequired || $this->requiredGroup > 0) { if($this->loginRequired || $this->requiredGroup > 0) {
$authorized = false; $apiKeyAuthorized = false;
if(isset($values['api_key']) && $this->apiKeyAllowed) { if(isset($values['api_key']) && $this->apiKeyAllowed) {
$apiKey = $values['api_key']; $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.'; $this->lastError = 'You are not logged in.';
header('HTTP 1.1 401 Unauthorized'); header('HTTP 1.1 401 Unauthorized');
return false; return false;
@ -125,6 +127,14 @@ class Request {
$this->lastError = "Insufficient permissions. Required group: ". GroupName($this->requiredGroup); $this->lastError = "Insufficient permissions. Required group: ". GroupName($this->requiredGroup);
header('HTTP 1.1 401 Unauthorized'); header('HTTP 1.1 401 Unauthorized');
return false; 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;
}
} }
} }

@ -17,6 +17,7 @@ class SetLanguage extends Request {
'langId' => new Parameter('langId', Parameter::TYPE_INT, true, NULL), 'langId' => new Parameter('langId', Parameter::TYPE_INT, true, NULL),
'langCode' => new StringType('langCode', 5, true, NULL), 'langCode' => new StringType('langCode', 5, true, NULL),
)); ));
$this->csrfTokenRequired = true;
} }
private function checkLanguage() { private function checkLanguage() {

@ -20,6 +20,7 @@ class Fetch extends Request {
$this->loginRequired = true; $this->loginRequired = true;
$this->requiredGroup = USER_GROUP_ADMIN; $this->requiredGroup = USER_GROUP_ADMIN;
$this->userCount = 0; $this->userCount = 0;
$this->csrfTokenRequired = true;
} }
private function getUserCount() { private function getUserCount() {

@ -0,0 +1,28 @@
<?php
namespace Api\User;
use \Api\Request;
class Info extends Request {
public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array());
$this->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;
}
}

@ -65,6 +65,7 @@ class Login extends Request {
if(!($this->success = $this->user->createSession($uid, $stayLoggedIn))) { if(!($this->success = $this->user->createSession($uid, $stayLoggedIn))) {
return $this->createError("Error creating Session: " . $sql->getLastError()); return $this->createError("Error creating Session: " . $sql->getLastError());
} else { } else {
$this->result["loggedIn"] = true;
$this->result['logoutIn'] = $this->user->getSession()->getExpiresSeconds(); $this->result['logoutIn'] = $this->user->getSession()->getExpiresSeconds();
$this->success = true; $this->success = true;
} }

@ -10,6 +10,7 @@ class Logout extends Request {
parent::__construct($user, $externalCall); parent::__construct($user, $externalCall);
$this->loginRequired = true; $this->loginRequired = true;
$this->apiKeyAllowed = false; $this->apiKeyAllowed = false;
$this->csrfTokenRequired = true;
} }
public function execute($values = array()) { public function execute($values = array()) {

@ -47,6 +47,7 @@ class CreateDatabase {
->addString("browser", 64) ->addString("browser", 64)
->addJson("data", false, '{}') ->addJson("data", false, '{}')
->addBool("stay_logged_in", true) ->addBool("stay_logged_in", true)
->addString("csrf_token", 16 )
->primaryKey("uid", "user_id") ->primaryKey("uid", "user_id")
->foreignKey("user_id", "User", "uid", new CascadeStrategy()); ->foreignKey("user_id", "User", "uid", new CascadeStrategy());

@ -6,4 +6,6 @@ abstract class StaticView {
public abstract function getCode(); public abstract function getCode();
public function __toString() { return $this->getCode(); }
} }

@ -21,7 +21,6 @@ abstract class View extends StaticView {
} }
public function getTitle() { return $this->title; } public function getTitle() { return $this->title; }
public function __toString() { return $this->getCode(); }
public function getDocument() { return $this->document; } public function getDocument() { return $this->document; }
public function isSearchable() { return $this->searchable; } public function isSearchable() { return $this->searchable; }
public function getReference() { return $this->reference; } public function getReference() { return $this->reference; }

@ -19,15 +19,17 @@ class Session extends ApiObject {
private ?string $os; private ?string $os;
private ?string $browser; private ?string $browser;
private bool $stayLoggedIn; 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->user = $user;
$this->sessionId = $sessionId; $this->sessionId = $sessionId;
$this->stayLoggedIn = true; $this->stayLoggedIn = true;
$this->csrfToken = $csrfToken ?? generateRandomString(16);
} }
public static function create($user, $stayLoggedIn) { public static function create($user, $stayLoggedIn) {
$session = new Session($user, null); $session = new Session($user, null, null);
if($session->insert($stayLoggedIn)) { if($session->insert($stayLoggedIn)) {
return $session; return $session;
} }
@ -85,6 +87,7 @@ class Session extends ApiObject {
'ipAddress' => $this->ipAddress, 'ipAddress' => $this->ipAddress,
'os' => $this->os, 'os' => $this->os,
'browser' => $this->browser, 'browser' => $this->browser,
'csrf_token' => $this->csrfToken
); );
} }
@ -93,7 +96,7 @@ class Session extends ApiObject {
$sql = $this->user->getSQL(); $sql = $this->user->getSQL();
$minutes = Session::DURATION; $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 $success = $sql
->insert("Session", $columns) ->insert("Session", $columns)
@ -104,7 +107,8 @@ class Session extends ApiObject {
$this->os, $this->os,
$this->browser, $this->browser,
json_encode($_SESSION), json_encode($_SESSION),
$stayLoggedIn) $stayLoggedIn,
$this->csrfToken)
->returning("uid") ->returning("uid")
->execute(); ->execute();
@ -135,8 +139,13 @@ class Session extends ApiObject {
->set("Session.os", $this->os) ->set("Session.os", $this->os)
->set("Session.browser", $this->browser) ->set("Session.browser", $this->browser)
->set("Session.data", json_encode($_SESSION)) ->set("Session.data", json_encode($_SESSION))
->set("Session.csrf_token", $this->csrfToken)
->where(new Compare("Session.uid", $this->sessionId)) ->where(new Compare("Session.uid", $this->sessionId))
->where(new Compare("Session.user_id", $this->user->getId())) ->where(new Compare("Session.user_id", $this->user->getId()))
->execute(); ->execute();
} }
public function getCsrfToken(): string {
return $this->csrfToken;
}
} }

@ -71,12 +71,19 @@ class User extends ApiObject {
} }
public function jsonSerialize() { public function jsonSerialize() {
return array( if ($this->isLoggedIn()) {
'uid' => $this->uid, return array(
'name' => $this->username, 'uid' => $this->uid,
'language' => $this->language, 'name' => $this->username,
'session' => $this->session, 'groups' => $this->groups,
); 'language' => $this->language->jsonSerialize(),
'session' => $this->session->jsonSerialize(),
);
} else {
return array(
'language' => $this->language->jsonSerialize(),
);
}
} }
private function reset() { private function reset() {
@ -116,7 +123,7 @@ class User extends ApiObject {
public function readData($userId, $sessionId, $sessionUpdate = true) { 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", $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") ->from("User")
->innerJoin("Session", "Session.user_id", "User.uid") ->innerJoin("Session", "Session.user_id", "User.uid")
->leftJoin("Language", "User.language_id", "Language.uid") ->leftJoin("Language", "User.language_id", "Language.uid")
@ -134,9 +141,10 @@ class User extends ApiObject {
$success = false; $success = false;
} else { } else {
$row = $res[0]; $row = $res[0];
$csrfToken = $row["csrf_token"];
$this->username = $row['name']; $this->username = $row['name'];
$this->uid = $userId; $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->setData(json_decode($row["data"] ?? '{}'));
$this->session->stayLoggedIn($row["stay_logged_in"]); $this->session->stayLoggedIn($row["stay_logged_in"]);
if($sessionUpdate) $this->session->update(); if($sessionUpdate) $this->session->update();