This commit is contained in:
2024-03-24 17:36:16 +01:00
parent aece0cb92a
commit 2ef4de0dba
17 changed files with 139 additions and 255 deletions

View File

@@ -9,7 +9,7 @@ use Core\Objects\Search\SearchQuery;
class Search extends Request {
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
"text" => new StringType("text", 32)
]);

View File

@@ -155,37 +155,4 @@ namespace Core\API\Settings {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to modify site settings");
}
}
class GenerateJWT extends SettingsAPI {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
"type" => new StringType("type", 32, true, "HS512")
]);
}
protected function _execute(): bool {
$algorithm = $this->getParam("type");
if (!Settings::isJwtAlgorithmSupported($algorithm)) {
return $this->createError("Algorithm is not supported");
}
$settings = $this->context->getSettings();
if (!$settings->generateJwtKey($algorithm)) {
return $this->createError("Error generating JWT-Key: " . $settings->getLogger()->getLastMessage());
}
$saveRequest = $settings->saveJwtKey($this->context);
if (!$saveRequest->success()) {
return $this->createError("Error saving JWT-Key: " . $saveRequest->getLastError());
}
$this->result["jwt_public_key"] = $settings->getJwtPublicKey(false)?->getKeyMaterial();
return true;
}
public static function getDefaultACL(Insert $insert): void {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to regenerate the JWT key. This invalidates all sessions");
}
}
}

View File

@@ -26,11 +26,6 @@ class Settings {
private array $allowedExtensions;
private string $timeZone;
// jwt
private ?string $jwtPublicKey;
private ?string $jwtSecretKey;
private string $jwtAlgorithm;
// recaptcha
private bool $recaptchaEnabled;
private string $recaptchaPublicKey;
@@ -84,22 +79,6 @@ class Settings {
}
}
public function getJwtPublicKey(bool $allowPrivate = true): ?\Firebase\JWT\Key {
if (empty($this->jwtPublicKey)) {
if ($allowPrivate && $this->jwtSecretKey) {
// we might have a symmetric key, should we instead return the private key?
return new \Firebase\JWT\Key($this->jwtSecretKey, $this->jwtAlgorithm);
}
return null;
} else {
return new \Firebase\JWT\Key($this->jwtPublicKey, $this->jwtAlgorithm);
}
}
public function getJwtSecretKey(): ?\Firebase\JWT\Key {
return $this->jwtSecretKey ? new \Firebase\JWT\Key($this->jwtSecretKey, $this->jwtAlgorithm) : null;
}
public function isInstalled(): bool {
return $this->installationComplete;
}
@@ -124,11 +103,6 @@ class Settings {
$settings->registrationAllowed = false;
$settings->timeZone = date_default_timezone_get();
// JWT
$settings->jwtSecretKey = null;
$settings->jwtPublicKey = null;
$settings->jwtAlgorithm = "HS256";
// Recaptcha
$settings->recaptchaEnabled = false;
$settings->recaptchaPublicKey = "";
@@ -143,50 +117,6 @@ class Settings {
return $settings;
}
public function generateJwtKey(string $algorithm = null): bool {
$this->jwtAlgorithm = $algorithm ?? $this->jwtAlgorithm;
// TODO: key encryption necessary?
if (in_array($this->jwtAlgorithm, ["HS256", "HS384", "HS512"])) {
$this->jwtSecretKey = generateRandomString(32);
$this->jwtPublicKey = null;
} else if (in_array($this->jwtAlgorithm, ["RS256", "RS384", "RS512"])) {
$bits = intval(substr($this->jwtAlgorithm, 2));
$private_key = openssl_pkey_new(["private_key_bits" => $bits]);
$this->jwtPublicKey = openssl_pkey_get_details($private_key)['key'];
openssl_pkey_export($private_key, $this->jwtSecretKey);
} else if (in_array($this->jwtAlgorithm, ["ES256", "ES384"])) {
// $ec = new \Elliptic\EC('secp256k1'); ??
$this->logger->error("JWT algorithm: '$this->jwtAlgorithm' is currently not supported.");
return false;
} else if ($this->jwtAlgorithm == "EdDSA") {
$keyPair = sodium_crypto_sign_keypair();
$this->jwtSecretKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
$this->jwtPublicKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
} else {
$this->logger->error("Invalid JWT algorithm: '$this->jwtAlgorithm', expected one of: " .
implode(",", array_keys(\Firebase\JWT\JWT::$supported_algs)));
return false;
}
return true;
}
public static function isJwtAlgorithmSupported(string $algorithm): bool {
return in_array(strtoupper($algorithm), ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "EDDSA"]);
}
public function saveJwtKey(Context $context): \Core\API\Settings\Set {
$req = new \Core\API\Settings\Set($context);
$req->execute(array("settings" => array(
"jwt_secret_key" => $this->jwtSecretKey,
"jwt_public_key" => $this->jwtSecretKey,
"jwt_algorithm" => $this->jwtAlgorithm,
)));
return $req;
}
public function loadFromDatabase(Context $context): bool {
$this->logger = new Logger("Settings", $context->getSQL());
$req = new \Core\API\Settings\Get($context);
@@ -199,9 +129,6 @@ class Settings {
$this->registrationAllowed = $result["user_registration_enabled"] ?? $this->registrationAllowed;
$this->installationComplete = $result["installation_completed"] ?? $this->installationComplete;
$this->timeZone = $result["time_zone"] ?? $this->timeZone;
$this->jwtSecretKey = $result["jwt_secret_key"] ?? $this->jwtSecretKey;
$this->jwtPublicKey = $result["jwt_public_key"] ?? $this->jwtPublicKey;
$this->jwtAlgorithm = $result["jwt_algorithm"] ?? $this->jwtAlgorithm;
$this->recaptchaEnabled = $result["recaptcha_enabled"] ?? $this->recaptchaEnabled;
$this->recaptchaPublicKey = $result["recaptcha_public_key"] ?? $this->recaptchaPublicKey;
$this->recaptchaPrivateKey = $result["recaptcha_private_key"] ?? $this->recaptchaPrivateKey;
@@ -210,13 +137,6 @@ class Settings {
$this->mailFooter = $result["mail_footer"] ?? $this->mailFooter;
$this->mailAsync = $result["mail_async"] ?? $this->mailAsync;
$this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions)));
if (!isset($result["jwt_secret_key"])) {
if ($this->generateJwtKey()) {
$this->saveJwtKey($context);
}
}
date_default_timezone_set($this->timeZone);
}
@@ -229,9 +149,6 @@ class Settings {
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false)
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true)
->addRow("time_zone", $this->timeZone, false, false)
->addRow("jwt_secret_key", $this->jwtSecretKey, true, false)
->addRow("jwt_public_key", $this->jwtPublicKey, false, false)
->addRow("jwt_algorithm", $this->jwtAlgorithm, false, false)
->addRow("recaptcha_enabled", $this->recaptchaEnabled ? "1" : "0", false, false)
->addRow("recaptcha_public_key", $this->recaptchaPublicKey, false, false)
->addRow("recaptcha_private_key", $this->recaptchaPrivateKey, true, false)

View File

@@ -6,7 +6,6 @@
"christian-riesen/base32": "^1.6",
"spomky-labs/cbor-php": "2.1.0",
"web-auth/cose-lib": "3.3.12",
"firebase/php-jwt": "^6.2",
"html2text/html2text": "^4.3"
},
"require-dev": {

View File

@@ -32,6 +32,7 @@ return [
"not_supported" => "Nicht unterstützt",
"yes" => "Ja",
"no" => "Nein",
"create_new" => "Erstellen",
# dialog / actions
"action" => "Aktion",

View File

@@ -15,6 +15,7 @@ return [
"not_supported" => "Not supported",
"yes" => "Yes",
"no" => "No",
"create_new" => "Create",
# dialog / actions
"action" => "Action",

View File

@@ -9,7 +9,6 @@ use Core\Driver\SQL\Condition\CondLike;
use Core\Driver\SQL\Condition\CondOr;
use Core\Driver\SQL\Join\InnerJoin;
use Core\Driver\SQL\SQL;
use Firebase\JWT\JWT;
use Core\Objects\DatabaseEntity\Language;
use Core\Objects\DatabaseEntity\Session;
use Core\Objects\DatabaseEntity\User;
@@ -99,28 +98,14 @@ class Context {
session_write_close();
}
private function loadSession(int $userId, int $sessionId): void {
$this->session = Session::init($this, $userId, $sessionId);
private function loadSession(string $sessionUUID): void {
$this->session = Session::init($this, $sessionUUID);
$this->user = $this->session?->getUser();
}
public function parseCookies(): void {
if (isset($_COOKIE['session']) && is_string($_COOKIE['session']) && !empty($_COOKIE['session'])) {
try {
$token = $_COOKIE['session'];
$settings = $this->configuration->getSettings();
$jwtKey = $settings->getJwtSecretKey();
if ($jwtKey) {
$decoded = (array)JWT::decode($token, $jwtKey);
$userId = ($decoded['userId'] ?? NULL);
$sessionId = ($decoded['sessionId'] ?? NULL);
if (!is_null($userId) && !is_null($sessionId)) {
$this->loadSession($userId, $sessionId);
}
}
} catch (\Exception $e) {
// ignored
}
$this->loadSession($_COOKIE['session']);
}
// set language by priority: 1. GET parameter, 2. cookie, 3. user's settings

View File

@@ -4,7 +4,6 @@ namespace Core\Objects\DatabaseEntity;
use DateTime;
use Exception;
use Firebase\JWT\JWT;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
use Core\Objects\DatabaseEntity\Attribute\Json;
@@ -21,6 +20,7 @@ class Session extends DatabaseEntity {
private User $user;
private DateTime $expires;
#[MaxLength(45)] private string $ipAddress;
#[MaxLength(36)] private string $uuid;
#[DefaultValue(true)] private bool $active;
#[MaxLength(64)] private ?string $os;
#[MaxLength(64)] private ?string $browser;
@@ -32,15 +32,21 @@ class Session extends DatabaseEntity {
parent::__construct();
$this->context = $context;
$this->user = $user;
$this->uuid = uuidv4();
$this->stayLoggedIn = false;
$this->csrfToken = $csrfToken ?? generateRandomString(16);
$this->expires = (new DateTime())->modify(sprintf("+%d second", Session::DURATION));
$this->active = true;
}
public static function init(Context $context, int $userId, int $sessionId): ?Session {
$session = Session::find($context->getSQL(), $sessionId, true, true);
if (!$session || !$session->active || $session->user->getId() !== $userId) {
public static function init(Context $context, string $sessionUUID): ?Session {
$sql = $context->getSQL();
$session = Session::findBy(Session::createBuilder($sql, true)
->fetchEntities(true)
->whereEq("Session.uuid", $sessionUUID)
->whereTrue("Session.active")
->whereGt("Session.expires", $sql->now()));
if (!$session) {
return null;
}
@@ -82,18 +88,13 @@ class Session extends DatabaseEntity {
}
}
public function getCookie(): string {
$this->updateMetaData();
$settings = $this->context->getSettings();
$token = ['userId' => $this->user->getId(), 'sessionId' => $this->getId()];
$jwtPublicKey = $settings->getJwtPublicKey();
return JWT::encode($token, $jwtPublicKey->getKeyMaterial(), $jwtPublicKey->getAlgorithm());
public function getUUID(): string {
return $this->uuid;
}
public function sendCookie(string $domain) {
$sessionCookie = $this->getCookie();
$secure = strcmp(getProtocol(), "https") === 0;
setcookie('session', $sessionCookie, $this->getExpiresTime(), "/", $domain, $secure, true);
setcookie('session', $this->uuid, $this->getExpiresTime(), "/", $domain, $secure, true);
}
public function getExpiresTime(): int {