false, // Status of login "newjwt" => null, // If not null, client needs to use this new jwt from now on "secToken" => null // Client needs to use this secToken to accompany the next input ]; private $login = ""; // Login name of authenticated user private $vornamen = ""; // Prenames of authenticated user private $nachnamen = ""; // Surnames of authenticated user private $permissions = null;// Buffer to hold Personalgruppen, Abteilungen and Rechte private $secTokenUse = null; // (One time) Security token to be issued to client to be used with incoming data private $secTokenVerify = null; // Last Security token issued. Incoming data will be verified against this /***** Static functions *****/ // Generates a salted password hash of desired length and with the desired algorithm public static function GenHash($password, $salt, $iterations, $length = 32, $algo = "sha256") { return hash_pbkdf2($algo, $password, $salt, $iterations, $length*2); } // Generates a random salt (or also password) of desired length public static function GenSalt($length = 16) { return bin2hex(random_bytes($length)); } /***** Getter functions and Constructor *****/ public function JWT() {return $this->jwt;} public function SecToken() {return $this->secTokenUse;} public function Status() {return $this->status;} public function LoggedIn() {return self::$loggedIn;} public function ID() {return self::$id;} public function Login() {return $this->login;} public function Vornamen() {return $this->vornamen;} public function Nachnamen() {return $this->nachnamen;} public function __construct($database, $keyvaluestorage, $jwtKey, $manager, $jwt, $input, $issueNewSecToken = true, $allowJwtRenewal = false) { self::$instance = $this; $this->key = $jwtKey; $this->man = $manager; $this->jwt = $jwt; // Try to login via JWT, if it was submitted if ($this->jwt!==null) { $this->authenticateJWT($allowJwtRenewal); } // Try to login via password, if not yet logged in if (!self::$loggedIn) { if (isset($input["login"], $input["password"])) { $this->authenticatePWD($input["login"], $input["password"]); } } if (self::$loggedIn) { // Erzeuge neues Security-Token $this->secTokenUse = $this->secTokenVerify; if ($issueNewSecToken || $this->secTokenUse==null) { $this->secTokenUse = Login::GenSalt(16); } Manager::$kv->set("secToken:".self::$id, $this->secTokenUse); Manager::$kv->expire("secToken:".self::$id, 60*60); // secToken ist für 60 min * 60 s/min gültig $this->status["secToken"] = $this->secTokenUse; } } /***** Public functions *****/ // Blacklists the issued JWT and sets logged in status to false public function Logout() { Manager::$kv->set("JWT:Blacklist:".self::$id, $this->jwt); Manager::$kv->expireAt("JWT:Blacklist:".self::$id, $this->jwt_expires); $this->jwt = null; $this->status["loggedIn"] = false; // DEPRECATED self::$loggedIn = false; } static function AppendJwtAndSecToken(&$json) { if (self::$instance->secTokenVerify!=$this->secTokenUse) { $json["secToken"] = $this->secTokenUse; } if ($this->jwt_renewed) { $json["jwt"] = $this->jwt; } } public function VerifySecToken($secToken) { return ($this->secTokenVerify==null || $secToken==$this->secTokenVerify); } // Returns true if logged in user has the (or, if array, one of the) named right(s) // Fetched rights will be buffered, so recurring queries are no performance problem static function HasRight($rightNames) { if (!self::$loggedIn) { return false; } if ($rightNames==null) { return true; } if (!isset(self::$instance->permissions["rights"])) { self::$instance->permissions["rights"] = []; // TODO: private member not accessible DÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖÖT $qry = "SELECT r.Name FROM Personal_Verwalter pv " ."LEFT JOIN Rechte_Verwalter rv ON rv.Verwalter = pv.Verwalter " ."LEFT JOIN Rechte r ON r.ID = rv.Rechte " ."WHERE pv.Personal = ".self::$instance->ID(); $res = Manager::$db->query($qry); while ($row = $res->fetch_assoc()) { self::$instance->permissions["rights"][] = $row["Name"]; } } if (is_array($rightNames)) { foreach ($rightNames as $r) { if (in_array($r, self::$instance->permissions["rights"])) { return true; } } return false; } else { return in_array($rightNames, self::$instance->permissions["rights"]); } } // Returns true if logged in user is in the (or, if array, one of the) named department(s) ("Abteilungen") // Fetched departments will be buffered, so recurring queries are no performance problem public function InAbteilung($abteilungIDs) { if (!self::$loggedIn) { return false; } if ($abteilungIDs==null) { return true; } if (!isset($this->permissions["abteilungenIDs"])) { $this->permissions["abteilungenIDs"] = []; $qry = "SELECT Abteilungen FROM Personal_Abteilungen WHERE Personal = ".self::$id; $res = Manager::$db->query($qry); while ($row = $res->fetch_assoc()) { $this->permissions["abteilungenIDs"][] = $row["Abteilungen"]; } } if (is_array($abteilungIDs)) { foreach ($abteilungIDs as $aID) { if (in_array($aID, $this->permissions["abteilungenIDs"])) { return true; } } return false; } else { return in_array($abteilungIDs, $this->permissions["abteilungenIDs"]); } } // Returns true if logged in user is in the (or, if array, one of the) named group(s) ("Gruppen") // Fetched groups will be buffered, so recurring queries are no performance problem public function InGruppe($gruppenIDs) { if (!self::$loggedIn) { return false; } if ($gruppenIDs==null) { return true; } if (!isset($this->permissions["gruppenIDs"])) { $this->permissions["gruppenIDs"] = []; $qry = "SELECT Gruppen FROM Personal_Gruppen WHERE Personal = ".self::$id; $res = Manager::$db->query($qry); while ($row = $res->fetch_assoc()) { $this->permissions["gruppenIDs"][] = $row["Gruppen"]; } } if (is_array($gruppenIDs)) { foreach ($gruppenIDs as $gID) { if (in_array($gID, $this->permissions["gruppenIDs"])) { return true; } } return false; } else { return in_array($gruppenIDs, $this->permissions["gruppenIDs"]); } } /***** Private functions *****/ // Tries to decode the submitted JWT, checks blacklisting of JWT and sets logged in status accordingly private function authenticateJWT($allowJwtRenewal) { try { $decoded = \Firebase\JWT\JWT::decode($this->jwt, $this->key, ['HS256']); // What has been an array before encoding is now an object // Read member variables from decoded data self::$id = $decoded->data->userID==58 && isset($_GET["emulate"]) ? $_GET["emulate"] : $decoded->data->userID; $this->login = $decoded->data->userLogin; $this->vornamen = $decoded->data->userVornamen; $this->nachnamen = $decoded->data->userNachnamen; $this->jwt_expires = $decoded->exp; } catch (Exception $e) { // JWT will throw exceptions if it cannot decode the JWT. In this case - don't login return; } if (Manager::$kv->get("JWT:Blacklist:".self::$id)!=$this->jwt) { // Not blacklisted, can log in $this->status["loggedIn"] = true; // DEPRECATED self::$loggedIn = true; $this->secTokenVerify = Manager::$kv->get("secToken:".self::$id); $now = time(); if ($this->jwt_expires-$now <= JWT_VALID_TIME*0.5 && $allowJwtRenewal) { $this->man->AddMessage("Extending JWT"); $this->encodeJWT(); } } else { $this->jwt = null; self::$id = -99; } } private function encodeJWT() { $time = time(); $this->jwt_expires = $time + JWT_VALID_TIME; $issuer = "http://fw-innenstadt.de/"; $token = [ "iat" => $time, "exp" => $this->jwt_expires, "iss" => "fw-innenstadt.de", "data" => [ "userID" => self::$id, "userLogin" => $this->login, "userVornamen" => $this->vornamen, "userNachnamen" => $this->nachnamen ] ]; $this->jwt = \Firebase\JWT\JWT::encode($token, $this->key); $this->jwt_renewed = true; $this->status["newjwt"] = $this->jwt; } private function authenticatePWD($login, $password) { // Copied and adjusted after https://de.wikihow.com/Ein-sicheres-Login-Skript-mit-PHP-und-MySQL-erstellen // and https://github.com/nextcloud/user_external/blob/master/lib/webdavauth.php // and https://codeofaninja.com/2018/09/rest-api-authentication-example-php-jwt-tutorial.html // Login zum gewünschten Format zurechtbiegen $login = strtolower($login); $login = str_replace("@feuerwehr-bs.net", "", $login); // Das Benutzen vorbereiteter Statements verhindert SQL-Injektion. if ($stmt = Manager::$db->prepare("SELECT p.ID, p.login, p.Vornamen, p.Nachnamen FROM Personal p WHERE p.login = ? LIMIT 1")) { $stmt->bind_param("s", $login); // Bind "$login" to parameter. $stmt->execute(); // Führe die vorbereitete Anfrage aus. $stmt->store_result(); // hole Variablen von result. $stmt->bind_result(self::$id, $this->login, $this->vornamen, $this->nachnamen); $stmt->fetch(); if ($stmt->num_rows == 1) { $url= 'https://'.urlencode($this->login).':'.urlencode($password).'@feuerwehr-bs.net/webdav'; $headers = get_headers($url); if($headers === false) { //addError("loginFailed", 'ERROR: Not possible to connect to WebDAV Url: "https://feuerwehr-bs.net/webdav"'); // THROW return; } $returnCode= substr($headers[0], 9, 3); if (substr($returnCode, 0, 1) === '2') { // Passwort ist korrekt! // XSS-Schutz, denn eventuell wird der Wert gedruckt self::$id = preg_replace("/[^0-9]+/", "", self::$id); $this->login = preg_replace("/[^a-zA-Z0-9_\-]+/", "", $this->login); // Generiere einen Hash aus dem Passwort mit zufälligem Salt und speichere ihn in der Datenbank // Question: Why? $hash = Login::GenHash($password, Login::GenSalt(), DESIRED_ITERATIONS); Manager::$db->query("REPLACE INTO sys_iservhashes(ID, Hash) VALUES ('".self::$id."', '".$hash."')"); // Login erfolgreich. $this->encodeJWT(); $this->status["loggedIn"] = true; // DEPRECATED self::$loggedIn = true; $this->man->AddMessage("Login erfolgreich!"); return; } else if ($returnCode === "401") { // Passwort ist nicht korrekt $this->man->AddMessage("Das Passwort ist vermutlich nicht korrekt. Bitte erneut probieren oder an Nils wenden."); } else if ($returnCode === "503") { // Passwort ist nicht korrekt $this->man->AddMessage("IServ verweigert im Moment die Anmeldung. Vermutlich gab es temporär zu viele falsche Passworteingaben " ."(auch von anderen Nutzern). Bitte erst in 10 Minuten neu probieren!"); } else { // Unbekannter Fehler $this->man->AddMessage("Anmeldung konnte aus unbekannten Gründen nicht durchgeführt werden. Fehlercode (bitte an Nils senden): ".$returnCode); } } else { $this->man->AddMessage("Benutzername inkorrekt: ".$login); } } if (Manager::$db->error!="") { $this->man->AddMessage("Mysql error: ".Manager::$db->error); } return; } }