323 lines
13 KiB
PHP
323 lines
13 KiB
PHP
<?php #lib/10_login.php
|
|
|
|
/********************************************************************************
|
|
* Content: Login class *
|
|
* Author: Nils Otterpohl *
|
|
* Last modification: 13.12.2020 *
|
|
* Version: alpha (object, incomplete, uncommented, untested) *
|
|
********************************************************************************/
|
|
|
|
class Login
|
|
{
|
|
public static $instance = null;
|
|
public static $id = ""; // ID of authenticated user
|
|
public static $loggedIn = false;
|
|
|
|
private $key; // JWT Server private key
|
|
private $man; // Owning API manager
|
|
|
|
private $jwt = null; // JSON Web Token (JWT), which was submitted or generated
|
|
private $jwt_expires = 0; // Expiration date of JWT
|
|
private $jwt_renewed = false;//
|
|
private $status = [ // Status array to be issued to client
|
|
"loggedIn" => 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;
|
|
}
|
|
|
|
} |