From e9fdf8e979f8d137d086d47d3b6c409f3d0ed02b Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Fri, 9 Feb 2024 15:09:04 +0100 Subject: [PATCH] kolab_sso: Install firebase/php-jwt via composer --- composer.json-dist | 9 +- phpstan.neon | 1 - plugins/kolab_sso/composer.json | 3 +- plugins/kolab_sso/config.inc.php.dist | 1 + plugins/kolab_sso/drivers/oauth2.php | 5 +- plugins/kolab_sso/kolab_sso.php | 5 - .../lib/Firebase/JWT/BeforeValidException.php | 7 - .../lib/Firebase/JWT/ExpiredException.php | 7 - plugins/kolab_sso/lib/Firebase/JWT/JWT.php | 382 ------------------ .../JWT/SignatureInvalidException.php | 7 - 10 files changed, 12 insertions(+), 415 deletions(-) delete mode 100644 plugins/kolab_sso/lib/Firebase/JWT/BeforeValidException.php delete mode 100644 plugins/kolab_sso/lib/Firebase/JWT/ExpiredException.php delete mode 100644 plugins/kolab_sso/lib/Firebase/JWT/JWT.php delete mode 100644 plugins/kolab_sso/lib/Firebase/JWT/SignatureInvalidException.php diff --git a/composer.json-dist b/composer.json-dist index 2cf5bdd6..836c0876 100644 --- a/composer.json-dist +++ b/composer.json-dist @@ -5,6 +5,10 @@ "require": { "php": ">=7.2.0", "caxy/php-htmldiff": "~0.1.15", + "endroid/qr-code": "~1.6.5", + "enygma/yubikey": "~3.2", + "firebase/php-jwt": "~6.4.0", + "kolab/net_ldap3": "dev-master", "lolli42/finediff": "~1.0.3", "pear/pear-core-minimal": "~1.10.1", "pear/net_socket": "~1.2.1", @@ -17,10 +21,7 @@ "pear/http_request2": "~2.5.0", "roundcube/rtf-html-php": "~2.1", "sabre/vobject": "~4.5.1", - "kolab/net_ldap3": "dev-master", - "spomky-labs/otphp": "~10.0.3", - "endroid/qr-code": "~1.6.5", - "enygma/yubikey": "~3.2" + "spomky-labs/otphp": "~10.0.3" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", diff --git a/phpstan.neon b/phpstan.neon index 06948b80..a0d7ff8a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -13,7 +13,6 @@ parameters: - '#Access to an undefined property (calendar|tasklist)::\$itip#' - '#Access to an undefined property (calendar|tasklist)::\$ical#' - '#Access to an undefined property Kolab2FA\\.*::\$(username|secret)#' - - '#Unsafe call to private method Firebase\\JWT\\JWT::.*#' - '#Access to an undefined property Sabre\\VObject.*::\$.*#' - '#Access to an undefined property DateTime::\$_dateonly#' # All expressions below are about libkolabxml (which I don't have installed) diff --git a/plugins/kolab_sso/composer.json b/plugins/kolab_sso/composer.json index 01e247d0..1a7c29a8 100644 --- a/plugins/kolab_sso/composer.json +++ b/plugins/kolab_sso/composer.json @@ -22,6 +22,7 @@ "php": ">=5.3.0", "roundcube/plugin-installer": ">=0.1.3", "kolab/libkolab": ">=3.4.0", - "pear/http_request2": "~2.5.0" + "pear/http_request2": "~2.5.0", + "firebase/php-jwt": "~6.4.0" } } diff --git a/plugins/kolab_sso/config.inc.php.dist b/plugins/kolab_sso/config.inc.php.dist index f037b14c..c548c039 100644 --- a/plugins/kolab_sso/config.inc.php.dist +++ b/plugins/kolab_sso/config.inc.php.dist @@ -42,6 +42,7 @@ $config['kolab_sso_options'] = array( // Client ID/Secret for the API 'client_id' => '20w6DXX69isNBaufCwyK24wkBHqPT2ht', 'client_secret' => 'd78McGW4UWfFyZprGd8BCKooll', + 'client_secret_algorithm' => 'RS256', // Token URI, if different than /token 'token_uri' => 'https://kolab.eu.auth0.com/oauth/token', // Authorize URI, if different than /authorize diff --git a/plugins/kolab_sso/drivers/oauth2.php b/plugins/kolab_sso/drivers/oauth2.php index 84a57cd7..c5b2c0fa 100644 --- a/plugins/kolab_sso/drivers/oauth2.php +++ b/plugins/kolab_sso/drivers/oauth2.php @@ -288,10 +288,13 @@ class kolab_sso_oauth2 } } + // TODO: Try all supported algorithms for a key instead of requiring a config value? + $key = new Firebase\JWT\Key($key, ($this->config['client_secret_algorithm'] ?? null) ?: 'RS256'); + $jwt = new Firebase\JWT\JWT(); $jwt::$leeway = 60; - $payload = $jwt->decode($token, $key, array_keys(Firebase\JWT\JWT::$supported_algs)); + $payload = $jwt->decode($token, $key); $result['email'] = $this->validate_token_payload($payload); } catch (Exception $e) { diff --git a/plugins/kolab_sso/kolab_sso.php b/plugins/kolab_sso/kolab_sso.php index fdedab73..71aa0f3c 100644 --- a/plugins/kolab_sso/kolab_sso.php +++ b/plugins/kolab_sso/kolab_sso.php @@ -348,11 +348,6 @@ class kolab_sso extends rcube_plugin ], true, true); } - // Add /lib to include_path - $include_path = $this->home . '/lib' . PATH_SEPARATOR; - $include_path .= ini_get('include_path'); - set_include_path($include_path); - require_once $this->home . "/drivers/$driver.php"; $this->debug = $this->rc->config->get('kolab_sso_debug'); diff --git a/plugins/kolab_sso/lib/Firebase/JWT/BeforeValidException.php b/plugins/kolab_sso/lib/Firebase/JWT/BeforeValidException.php deleted file mode 100644 index c147852b..00000000 --- a/plugins/kolab_sso/lib/Firebase/JWT/BeforeValidException.php +++ /dev/null @@ -1,7 +0,0 @@ - - * @author Anant Narayanan - * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD - * @link https://github.com/firebase/php-jwt - */ -class JWT -{ - /** - * When checking nbf, iat or expiration times, - * we want to provide some extra leeway time to - * account for clock skew. - */ - public static $leeway = 0; - - /** - * Allow the current timestamp to be specified. - * Useful for fixing a value within unit testing. - * - * Will default to PHP time() value if null. - */ - public static $timestamp = null; - - public static $supported_algs = [ - 'HS256' => ['hash_hmac', 'SHA256'], - 'HS512' => ['hash_hmac', 'SHA512'], - 'HS384' => ['hash_hmac', 'SHA384'], - 'RS256' => ['openssl', 'SHA256'], - 'RS384' => ['openssl', 'SHA384'], - 'RS512' => ['openssl', 'SHA512'], - ]; - - /** - * Decodes a JWT string into a PHP object. - * - * @param string $jwt The JWT - * @param string|array $key The key, or map of keys. - * If the algorithm used is asymmetric, this is the public key - * @param array $allowed_algs List of supported verification algorithms - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' - * - * @return object The JWT's payload as a PHP object - * - * @throws UnexpectedValueException Provided JWT was invalid - * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed - * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' - * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' - * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim - * - * @uses jsonDecode - * @uses urlsafeB64Decode - */ - public static function decode($jwt, $key, array $allowed_algs = []) - { - $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp; - - if (empty($key)) { - throw new InvalidArgumentException('Key may not be empty'); - } - $tks = explode('.', $jwt); - if (count($tks) != 3) { - throw new UnexpectedValueException('Wrong number of segments'); - } - [$headb64, $bodyb64, $cryptob64] = $tks; - if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { - throw new UnexpectedValueException('Invalid header encoding'); - } - if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { - throw new UnexpectedValueException('Invalid claims encoding'); - } - if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { - throw new UnexpectedValueException('Invalid signature encoding'); - } - if (empty($header->alg)) { - throw new UnexpectedValueException('Empty algorithm'); - } - if (empty(static::$supported_algs[$header->alg])) { - throw new UnexpectedValueException('Algorithm not supported'); - } - if (!in_array($header->alg, $allowed_algs)) { - throw new UnexpectedValueException('Algorithm not allowed'); - } - if (is_array($key) || $key instanceof \ArrayAccess) { - if (isset($header->kid)) { - if (!isset($key[$header->kid])) { - throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); - } - $key = $key[$header->kid]; - } else { - throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); - } - } - - // Check the signature - if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { - throw new SignatureInvalidException('Signature verification failed'); - } - - // Check if the nbf if it is defined. This is the time that the - // token can actually be used. If it's not yet that time, abort. - if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { - throw new BeforeValidException( - 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) - ); - } - - // Check that this token has been created before 'now'. This prevents - // using tokens that have been created for later use (and haven't - // correctly used the nbf claim). - if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { - throw new BeforeValidException( - 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) - ); - } - - // Check if this token has expired. - if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { - throw new ExpiredException('Expired token'); - } - - return $payload; - } - - /** - * Converts and signs a PHP object or array into a JWT string. - * - * @param object|array $payload PHP object or array - * @param string $key The secret key. - * If the algorithm used is asymmetric, this is the private key - * @param string $alg The signing algorithm. - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' - * @param mixed $keyId - * @param array $head An array with header elements to attach - * - * @return string A signed JWT - * - * @uses jsonEncode - * @uses urlsafeB64Encode - */ - public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) - { - $header = ['typ' => 'JWT', 'alg' => $alg]; - if ($keyId !== null) { - $header['kid'] = $keyId; - } - if (isset($head) && is_array($head)) { - $header = array_merge($head, $header); - } - $segments = []; - $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); - $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); - $signing_input = implode('.', $segments); - - $signature = static::sign($signing_input, $key, $alg); - $segments[] = static::urlsafeB64Encode($signature); - - return implode('.', $segments); - } - - /** - * Sign a string with a given key and algorithm. - * - * @param string $msg The message to sign - * @param string|resource $key The secret key - * @param string $alg The signing algorithm. - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' - * - * @return string An encrypted message - * - * @throws DomainException Unsupported algorithm was specified - */ - public static function sign($msg, $key, $alg = 'HS256') - { - if (empty(static::$supported_algs[$alg])) { - throw new DomainException('Algorithm not supported'); - } - - [$function, $algorithm] = static::$supported_algs[$alg]; - - switch($function) { - case 'hash_hmac': - return hash_hmac($algorithm, $msg, $key, true); - case 'openssl': - $signature = ''; - $success = openssl_sign($msg, $signature, $key, $algorithm); - if (!$success) { - throw new DomainException("OpenSSL unable to sign data"); - } else { - return $signature; - } - } - - throw new DomainException("Invalid algorithm"); - } - - /** - * Verify a signature with the message, key and method. Not all methods - * are symmetric, so we must have a separate verify and sign method. - * - * @param string $msg The original message (header and body) - * @param string $signature The original signature - * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key - * @param string $alg The algorithm - * - * @return bool - * - * @throws DomainException Invalid Algorithm or OpenSSL failure - */ - private static function verify($msg, $signature, $key, $alg) - { - if (empty(static::$supported_algs[$alg])) { - throw new DomainException('Algorithm not supported'); - } - - [$function, $algorithm] = static::$supported_algs[$alg]; - switch($function) { - case 'openssl': - $success = openssl_verify($msg, $signature, $key, $algorithm); - if ($success === 1) { - return true; - } elseif ($success === 0) { - return false; - } - // returns 1 on success, 0 on failure, -1 on error. - throw new DomainException( - 'OpenSSL error: ' . openssl_error_string() - ); - case 'hash_hmac': - default: - $hash = hash_hmac($algorithm, $msg, $key, true); - if (function_exists('hash_equals')) { - return hash_equals($signature, $hash); - } - $len = min(static::safeStrlen($signature), static::safeStrlen($hash)); - - $status = 0; - for ($i = 0; $i < $len; $i++) { - $status |= (ord($signature[$i]) ^ ord($hash[$i])); - } - $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); - - return ($status === 0); - } - } - - /** - * Decode a JSON string into a PHP object. - * - * @param string $input JSON string - * - * @return object Object representation of JSON string - * - * @throws DomainException Provided string was invalid JSON - */ - public static function jsonDecode($input) - { - if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { - /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you - * to specify that large ints (like Steam Transaction IDs) should be treated as - * strings, rather than the PHP default behaviour of converting them to floats. - */ - $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING); - } else { - /** Not all servers will support that, however, so for older versions we must - * manually detect large ints in the JSON string and quote them (thus converting - *them to strings) before decoding, hence the preg_replace() call. - */ - $max_int_length = strlen((string) PHP_INT_MAX) - 1; - $json_without_bigints = preg_replace('/:\s*(-?\d{' . $max_int_length . ',})/', ': "$1"', $input); - $obj = json_decode($json_without_bigints); - } - - if (function_exists('json_last_error') && $errno = json_last_error()) { - static::handleJsonError($errno); - } elseif ($obj === null && $input !== 'null') { - throw new DomainException('Null result with non-null input'); - } - return $obj; - } - - /** - * Encode a PHP object into a JSON string. - * - * @param object|array $input A PHP object or array - * - * @return string JSON representation of the PHP object or array - * - * @throws DomainException Provided object could not be encoded to valid JSON - */ - public static function jsonEncode($input) - { - $json = json_encode($input); - if (function_exists('json_last_error') && $errno = json_last_error()) { - static::handleJsonError($errno); - } elseif ($json === 'null' && $input !== null) { - throw new DomainException('Null result with non-null input'); - } - return $json; - } - - /** - * Decode a string with URL-safe Base64. - * - * @param string $input A Base64 encoded string - * - * @return string A decoded string - */ - public static function urlsafeB64Decode($input) - { - $remainder = strlen($input) % 4; - if ($remainder) { - $padlen = 4 - $remainder; - $input .= str_repeat('=', $padlen); - } - return base64_decode(strtr($input, '-_', '+/')); - } - - /** - * Encode a string with URL-safe Base64. - * - * @param string $input The string you want encoded - * - * @return string The base64 encode of what you passed in - */ - public static function urlsafeB64Encode($input) - { - return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); - } - - /** - * Helper method to create a JSON error. - * - * @param int $errno An error number from json_last_error() - * - * @return void - */ - private static function handleJsonError($errno) - { - $messages = [ - JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', - JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', - JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', - JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', - JSON_ERROR_UTF8 => 'Malformed UTF-8 characters', //PHP >= 5.3.3 - ]; - throw new DomainException( - $messages[$errno] - ?? 'Unknown JSON error: ' . $errno - ); - } - - /** - * Get the number of bytes in cryptographic strings. - * - * @param string $str - * - * @return int - */ - private static function safeStrlen($str) - { - if (function_exists('mb_strlen')) { - return mb_strlen($str, '8bit'); - } - return strlen($str); - } -} diff --git a/plugins/kolab_sso/lib/Firebase/JWT/SignatureInvalidException.php b/plugins/kolab_sso/lib/Firebase/JWT/SignatureInvalidException.php deleted file mode 100644 index d35dee9f..00000000 --- a/plugins/kolab_sso/lib/Firebase/JWT/SignatureInvalidException.php +++ /dev/null @@ -1,7 +0,0 @@ -