diff --git a/app/Core/Configuration/DefaultConfig.php b/app/Core/Configuration/DefaultConfig.php index 4f3a0c136..a42af85b4 100644 --- a/app/Core/Configuration/DefaultConfig.php +++ b/app/Core/Configuration/DefaultConfig.php @@ -384,6 +384,11 @@ class DefaultConfig */ public string $oidcClientSecret = ''; + /** + * @var string Custom Auto discover URL + */ + public string $oidcAutoDiscoverUrl = ''; + /** * @var string OIDC Auth URL */ diff --git a/app/Core/Configuration/Environment.php b/app/Core/Configuration/Environment.php index 3799a0b72..8d3bf1fe9 100644 --- a/app/Core/Configuration/Environment.php +++ b/app/Core/Configuration/Environment.php @@ -65,6 +65,7 @@ class Environment implements ArrayAccess, ConfigContract 'ldapDomain' => 'LEAN_LDAP_LDAP_DOMAIN', 'oidcClientId' => 'LEAN_OIDC_CLIENT_ID', 'oidcClientSecret' => 'LEAN_OIDC_CLIENT_SECRET', + 'oidcAutoDiscoverUrl' => 'LEAN_OIDC_AUTO_DISCOVER', 'oidcAuthUrl' => 'LEAN_OIDC_AUTH_URL_OVERRIDE', 'oidcTokenUrl' => 'LEAN_OIDC_TOKEN_URL_OVERRIDE', 'oidcJwksUrl' => 'LEAN_OIDC_JWKS_URL_OVERRIDE', diff --git a/app/Domain/Oidc/Services/Oidc.php b/app/Domain/Oidc/Services/Oidc.php index dd1431a12..90ebb7e8d 100644 --- a/app/Domain/Oidc/Services/Oidc.php +++ b/app/Domain/Oidc/Services/Oidc.php @@ -14,6 +14,8 @@ use Leantime\Domain\Users\Repositories\Users as UserRepository; use OpenSSLAsymmetricKey; use Symfony\Component\HttpFoundation\Response; +use phpseclib3\Crypt\PublicKeyLoader; +use phpseclib3\Math\BigInteger; /** * @@ -28,6 +30,7 @@ class Oidc private bool $configLoaded = false; private string $providerUrl = ''; + private string $autoDiscoverUrl = ''; private string $clientId = ''; private string $clientSecret = ''; private string $authUrl = ''; @@ -75,6 +78,7 @@ public function __construct( $providerUrl = $this->config->get('oidcProviderUrl'); $this->providerUrl = !empty($providerUrl) ? $this->trimTrailingSlash($providerUrl) : $providerUrl; + $this->autoDiscoverUrl = $this->config->get('oidcAutoDiscoverUrl', ''); $this->clientId = $this->config->get('oidcClientId', ''); $this->clientSecret = $this->config->get('oidcClientSecret', ''); $this->authUrl = $this->config->get('oidcAuthUrl', ''); @@ -153,7 +157,7 @@ public function callback(string $code, string $state): Response if (isset($tokens['id_token'])) { $userInfo = $this->decodeJWT($tokens['id_token']); } elseif (isset($tokens['access_token'])) { - //falback to OAuth userinfo endpoint + //fallback to OAuth userinfo endpoint $userInfo = $this->pollUserInfo($tokens['access_token']); } else { $this->displayError("oidc.error.unsupportedToken"); @@ -373,10 +377,9 @@ private function getPublicKey(string $kid): OpenSSLAsymmetricKey|false return openssl_pkey_get_public(file_get_contents($this->certificateFile)); } - - $httpClient = new Client(); - $response = $httpClient->get($this->getJwksUrl()); + // AUTH HEADER? + $response = $httpClient->get($this->getJwksUrl()); // https://cloud.lukas-sieper.de/apps/oidc/jwks $keys = json_decode($response->getBody()->getContents(), true); if (isset($keys['keys'])) { $keys = $keys['keys']; @@ -394,7 +397,12 @@ private function getPublicKey(string $kid): OpenSSLAsymmetricKey|false $keySource = ''; if (isset($key['x5c'])) { $keySource = '-----BEGIN CERTIFICATE-----' . PHP_EOL . chunk_split($key['x5c'][0], 64, PHP_EOL) . '-----END CERTIFICATE-----'; - } elseif (isset($key['n'])) { + } elseif (isset($key['n']) && isset($key['e'])) { + // Parse the public key from n and e + $modulus = $this->base64UrlDecode($key['n']); + $exponent = $this->base64UrlDecode($key['e']); + $keySource = $this->createPublicKey($modulus, exponent: $exponent); + } else { $this->displayError('oidc.error.unsupportedKeyFormat'); } } @@ -407,6 +415,29 @@ private function getPublicKey(string $kid): OpenSSLAsymmetricKey|false return false; } + private function base64UrlDecode(string $input): string + { + $remainder = strlen($input) % 4; + if ($remainder) { + $padlen = 4 - $remainder; + $input .= str_repeat('=', $padlen); + } + return base64_decode(strtr($input, '-_', '+/')); + } + + //key to PEM + private function createPublicKey(string $modulus, string $exponent): string + { + + $rsa = PublicKeyLoader::load([ + 'e' => new BigInteger($exponent, 256), + 'n' => new BigInteger($modulus, 256), + ]); + + return $rsa->__toString(); + + } + /** * @return string * @throws GuzzleException @@ -444,7 +475,9 @@ private function loadEndpoints(): bool $httpClient = new Client(); try { - $response = $httpClient->get($this->providerUrl . '/.well-known/openid-configuration'); + // $uri = strlen() ? $this->autoDiscoverUrl : $this->providerUrl; + $uri = empty($this->autoDiscoverUrl) ? $this->providerUrl : $this->autoDiscoverUrl; + $response = $httpClient->get($uri . '/.well-known/openid-configuration'); $endpoints = json_decode($response->getBody()->getContents(), true); }catch(\Exception $e) { report($e); diff --git a/composer.json b/composer.json index 01a853ab2..0683550e4 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,6 @@ "ext-pdo": "*", "ext-zip": "*", "ext-fileinfo": "*", - "guzzlehttp/guzzle": "^7.9.2", "aws/aws-sdk-php": "^3.314", "phpmailer/phpmailer": "6.6.0", @@ -83,7 +82,9 @@ "ext-fileinfo": "*", "sentry/sdk": "^3.5", - "predis/predis": "^2.2" + "predis/predis": "^2.2", + "phpseclib/phpseclib": "~3.0" + }, "require-dev": { "squizlabs/php_codesniffer": "^3.8", diff --git a/composer.lock b/composer.lock index 34918752f..dcbfc565c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "01c57357178e838d380e6138c84c72cb", + "content-hash": "b0df6cae92ad474b5a6675039b911231", "packages": [ { "name": "aws/aws-crt-php", @@ -3802,6 +3802,123 @@ ], "time": "2023-04-27T10:17:12+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2024-05-08T12:36:18+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "php-http/client-common", "version": "2.7.1", @@ -4336,6 +4453,116 @@ ], "time": "2024-07-20T21:41:07+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.42", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.42" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2024-09-16T03:06:04+00:00" + }, { "name": "predis/predis", "version": "v2.2.2", @@ -12201,7 +12428,7 @@ "ext-zip": "*", "ext-fileinfo": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.1.13" },