Database['Driver']) { # case 'sqlite': # $database = new PDO('sqlite:' . $settings->Database['Path']); if (is_writable($settings->Sqlite['Path'])) { $pdoDB = new PDO('sqlite:' . $settings->Sqlite['Path']); } else { throw new Exception(sprintf('Database file \'%1$s\' is not writable', $settings->Sqlite['Path'])); } # } } catch (Exception $e) { throw new Exception(sprintf('Unable to connect to database \'%1$s\'', $settings->Sqlite['Path'])); } function authenticateLDAP (string $username, string $password) { global $settings; if (!empty($username) && !empty($password)) { // Handle login requests $ds = ldap_connect($settings->LDAP['Server'], $settings->LDAP['Port']); // Strict namingconvention: only allow alphabetic characters $sanitizedUsername = preg_replace('([^a-zA-Z]*)', '', $_POST['username']); $qualifiedUsername = $settings->LDAP['Domain'] . '\\' . $sanitizedUsername; if (@ldap_bind($ds, $qualifiedUsername, utf8_encode($_POST['password']))) { // Successful authentication; get additional userdetails from authenticationsource $ldapSearchResults = ldap_search($ds, $settings->LDAP['BaseDN'], "sAMAccountName=$sanitizedUsername"); $commonName = ldap_get_entries($ds, $ldapSearchResults)[0]['cn'][0]; $browserDetails = get_browser(null, True); $geoLocation = json_decode(file_get_contents("http://ip-api.com/json/{$_SERVER['HTTP_X_REAL_IP']}")); if ($geoLocation->status === 'fail') { switch ($geoLocation->message) { case 'private range': case 'reserved range': $geoLocation = json_decode(file_get_contents("http://ip-api.com/json/" . trim(file_get_contents('https://api.ipify.org')) )); break; case 'invalid query': default: $geoLocation->city = null; $geoLocation->countryCode = null; break; } } // Create JWT-payload $jwtPayload = [ 'iat' => time(), // Issued at: time when the token was generated 'iss' => $_SERVER['SERVER_NAME'], // Issuer 'sub' => $qualifiedUsername, // Subject (ie. username) 'name' => $commonName, // Common name (as retrieved from AD) 'fp' => base64_encode(json_encode((object) [ // Fingerprint 'browser' => $browserDetails['browser'], 'platform' => $browserDetails['platform'], 'city' => $geoLocation->city, 'countrycode' => $geoLocation->countryCode ])) ]; $secureToken = JWT::encode($jwtPayload, base64_decode($settings->JWT['PrivateKey_base64'])); return ['status' => 'Success', 'token' => $secureToken]; } else { // LDAP authentication failed! return ['status' => 'Fail', 'reason' => '1']; } } else { // Empty username or passwords not allowed! return ['status' => 'Fail', 'reason' => '1']; } } function storeToken (string $secureToken, string $qualifiedUsername, string $httpHost) { global $settings, $pdoDB; // Save authentication token in database serverside try { $pdoQuery = $pdoDB->prepare(' INSERT INTO SecureToken (UserId, Value) SELECT User.Id, :securetoken FROM User WHERE LOWER(User.Username) = :qualifiedusername '); $pdoQuery->execute([ ':securetoken' => $secureToken, ':qualifiedusername' => strtolower($qualifiedUsername) ]); } catch (Exception $e) { return ['status' => 'Fail', 'reason' => $e]; } // Save authentication token in cookie clientside $cookieDomain = array_values(array_filter($settings->Session['CookieDomains'], function ($value) use ($httpHost) { // Check if $_SERVER['HTTP_HOST'] matches any of the configured domains (either explicitly or as a subdomain) // This might seem backwards, but relying on $_SERVER directly allows spoofed values with potential security risks return (strlen($value) > strlen($httpHost)) ? false : (0 === substr_compare($httpHost, $value, -strlen($value))); }))[0]; if ($cookieDomain && setcookie('JWT', $secureToken, (time() + $settings->Session['Duration']), '/', '.' . $cookieDomain)) { return ['status' => 'Success']; } else { return ['status' => 'Fail', 'reason' => 'Unable to store cookie(s)']; } } function validateToken (string $secureToken) { global $settings, $pdoDB; // Decode provided authentication token try { $jwtPayload = JWT::decode($secureToken, base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); } catch (Exception $e) { // Invalid token if ($settings->Debug['LogToFile']) { file_put_contents('../validateToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Provided token could not be decoded' . PHP_EOL, FILE_APPEND); } return ['status' => 'Fail', 'reason' => '1']; } if ((int)$jwtPayload->iat < (time() - (int)$settings->Session['Duration'])) { // Expired token if ($settings->Debug['LogToFile']) { file_put_contents('../validateToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Provided token has expired' . PHP_EOL, FILE_APPEND); } return ['status' => 'Fail', 'reason' => '3']; } // Retrieve all authentication tokens from database matching username $pdoQuery = $pdoDB->prepare(' SELECT User.Id, SecureToken.Value FROM SecureToken LEFT JOIN User ON (User.Id=SecureToken.UserId) WHERE LOWER(User.Username) = :username '); $pdoQuery->execute([ ':username' => (string) strtolower($jwtPayload->sub) ]); foreach($pdoQuery->fetchAll(PDO::FETCH_ASSOC) as $row) { try { $storedTokens[] = JWT::decode($row['Value'], base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); $currentUserId = $row['Id']; } catch (Exception $e) { continue; } } // Compare provided authentication token to all stored tokens in database if (!empty($storedTokens) && sizeof(array_filter($storedTokens, function ($value) use ($jwtPayload) { return $value->iat === $jwtPayload->iat; })) === 1) { purgeTokens($currentUserId, $settings->Session['Duration']); return [ 'status' => 'Success', 'name' => $jwtPayload->name, 'uid' => $currentUserId ]; } else { if ($settings->Debug['LogToFile']) { file_put_contents('../validateToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Either no matching token or multiple matching tokens found in database' . PHP_EOL, FILE_APPEND); } return ['status' => 'Fail', 'reason' => '2']; } } function purgeTokens(int $userID, int $maximumTokenAge) { global $settings, $pdoDB; $defunctTokens = []; $expiredTokens = []; $pdoQuery = $pdoDB->prepare(' SELECT SecureToken.Id, SecureToken.Value FROM SecureToken WHERE SecureToken.UserId = :userid '); $pdoQuery->execute([ ':userid' => (int) $userID ]); foreach($pdoQuery->fetchAll(PDO::FETCH_ASSOC) as $row) { try { $token = JWT::decode($row['Value'], base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); if ($token->iat < (time() - $maximumTokenAge)) { $expiredTokens[] = $row['Id']; } } catch (Exception $e) { $defunctTokens[] = $row['Id']; } } try { // Sadly, PDO does not support named parameters in constructions like 'IN ( :array )' // instead, the supported syntax is unnamed placeholders like 'IN (?, ?, ?, ...)' $pdoQuery = $pdoDB->prepare(' DELETE FROM SecureToken WHERE SecureToken.Id IN (' . implode( ',', array_fill(0, count(array_merge($defunctTokens, $expiredTokens)), '?')) . ') '); $pdoQuery->execute(array_merge($defunctTokens, $expiredTokens)); if ($settings->Debug['LogToFile']) { file_put_contents('../purgeToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Garbage collection succeeded (' . $userID . ' => ' . $pdoQuery->rowCount() . ')' . PHP_EOL, FILE_APPEND); } return [ 'status' => 'Success', 'amount' => $pdoQuery->rowCount() ]; } catch (Exception $e) { if ($settings->Debug['LogToFile']) { file_put_contents('../purgeToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Garbage collection failed (' . $userID . ' => ' . $e . ')' . PHP_EOL, FILE_APPEND); } return ['status' => 'Fail', 'reason' => $e]; } } ?>