diff --git a/lib/Sdk/Storage/Storage.php b/lib/Sdk/Storage/Storage.php index d1fc9eb..6c2374e 100644 --- a/lib/Sdk/Storage/Storage.php +++ b/lib/Sdk/Storage/Storage.php @@ -171,11 +171,13 @@ static function setJwksUrl($jwksUrl) /** * Gets cached JWKS data if available and not expired * + * @param string|null $jwksUrl Optional JWKS URL to namespace the cache * @return array|null The cached JWKS data or null if not available/expired */ - static function getCachedJwks() + static function getCachedJwks(?string $jwksUrl = null) { - $cachedData = self::getItem(StorageEnums::JWKS_CACHE); + $cacheKey = self::getJwksCacheKey($jwksUrl); + $cachedData = self::getItem($cacheKey); if (empty($cachedData)) { return null; } @@ -187,7 +189,7 @@ static function getCachedJwks() // Check if cache has expired if ($data['expires_at'] < time()) { - self::removeItem(StorageEnums::JWKS_CACHE); + self::removeItem($cacheKey); return null; } @@ -199,26 +201,30 @@ static function getCachedJwks() * * @param array $jwks The JWKS data to cache * @param int $ttlSeconds TTL in seconds (default: 1 hour) + * @param string|null $jwksUrl Optional JWKS URL to namespace the cache * @return void */ - static function setCachedJwks(array $jwks, int $ttlSeconds = 3600) + static function setCachedJwks(array $jwks, int $ttlSeconds = 3600, ?string $jwksUrl = null) { + $cacheKey = self::getJwksCacheKey($jwksUrl); $cacheData = [ 'jwks' => $jwks, 'expires_at' => time() + $ttlSeconds ]; - self::setItem(StorageEnums::JWKS_CACHE, json_encode($cacheData), time() + $ttlSeconds); + self::setItem($cacheKey, json_encode($cacheData), time() + $ttlSeconds); } /** * Clears the cached JWKS data * + * @param string|null $jwksUrl Optional JWKS URL to namespace the cache * @return void */ - static function clearCachedJwks() + static function clearCachedJwks(?string $jwksUrl = null) { - self::removeItem(StorageEnums::JWKS_CACHE); + $cacheKey = self::getJwksCacheKey($jwksUrl); + self::removeItem($cacheKey); } /** @@ -284,4 +290,16 @@ static function setPersistentCookieDuration(int $duration) { self::$persistentCookieDuration = $duration; } + + /** + * Build a cache key for JWKS, optionally namespaced by URL. + */ + private static function getJwksCacheKey(?string $jwksUrl = null): string + { + if (empty($jwksUrl)) { + return StorageEnums::JWKS_CACHE; + } + + return StorageEnums::JWKS_CACHE . '_' . md5($jwksUrl); + } } diff --git a/lib/Sdk/Utils/Utils.php b/lib/Sdk/Utils/Utils.php index 54807c5..625ce24 100644 --- a/lib/Sdk/Utils/Utils.php +++ b/lib/Sdk/Utils/Utils.php @@ -94,13 +94,21 @@ static public function validationURL(string $url) * * @return array|null The decoded payload as an associative array, or null if the token is invalid. */ - static public function parseJWT(string $token) + static public function parseJWT(string $token, ?string $jwksUrl = null) { + $jwks = null; + $jwks_url = $jwksUrl; + try { - $jwks_url = Storage::getInstance()->getJwksUrl(); + if ($jwks_url === null) { + $jwks_url = Storage::getInstance()->getJwksUrl(); + } + + // Prevent fetching JWKS from an unexpected domain + $jwks_url = self::validateTrustedJwksUrl($jwks_url); // Try to get cached JWKS first - $jwks = Storage::getInstance()->getCachedJwks(); + $jwks = Storage::getInstance()->getCachedJwks($jwks_url); if ($jwks === null) { // Cache miss - fetch from server @@ -109,7 +117,7 @@ static public function parseJWT(string $token) if ($jwks && isset($jwks['keys'])) { // Cache the JWKS for 1 hour (3600 seconds) - Storage::getInstance()->setCachedJwks($jwks, 3600); + Storage::getInstance()->setCachedJwks($jwks, 3600, $jwks_url); } } @@ -122,12 +130,12 @@ static public function parseJWT(string $token) // If parsing fails with cached JWKS, try to refresh from server if ($jwks !== null) { try { - Storage::getInstance()->clearCachedJwks(); + Storage::getInstance()->clearCachedJwks($jwks_url); $jwks_json = file_get_contents($jwks_url); $jwks = json_decode($jwks_json, true); if ($jwks && isset($jwks['keys'])) { - Storage::getInstance()->setCachedJwks($jwks, 3600); + Storage::getInstance()->setCachedJwks($jwks, 3600, $jwks_url); return json_decode(json_encode(JWT::decode($token, JWK::parseKeySet($jwks))), true); } } catch (Exception $refreshException) { @@ -139,6 +147,45 @@ static public function parseJWT(string $token) } } + /** + * Validates that the JWKS URL uses HTTPS and matches the configured domain. + * + * @param string $jwksUrl The JWKS URL to validate. + * + * @return string The validated JWKS URL. + * + * @throws InvalidArgumentException If the URL is invalid, uses a non-HTTPS scheme, + * or does not match the configured JWKS host. + */ + static public function validateTrustedJwksUrl(string $jwksUrl): string + { + $parts = parse_url($jwksUrl); + + if ($parts === false || empty($parts['scheme']) || empty($parts['host'])) { + throw new InvalidArgumentException('Invalid JWKS URL'); + } + + if (strtolower($parts['scheme']) !== 'https') { + throw new InvalidArgumentException('JWKS URL must use https'); + } + + // Enforce host match; fail closed when no trusted domain is configured + try { + $trustedJwksUrl = Storage::getInstance()->getJwksUrl(); + $trustedHost = parse_url($trustedJwksUrl, PHP_URL_HOST); + } catch (\LogicException $e) { + throw new InvalidArgumentException( + 'Cannot validate JWKS URL: no trusted domain configured. Initialize the SDK first or provide a pre-validated URL.' + ); + } + + if (empty($trustedHost) || strcasecmp($parts['host'], $trustedHost) !== 0) { + throw new InvalidArgumentException('Untrusted JWKS domain'); + } + + return $jwksUrl; + } + /** * Checks and validates additional parameters provided as an associative array. * diff --git a/lib/Webhooks/WebhookDecoder.php b/lib/Webhooks/WebhookDecoder.php new file mode 100644 index 0000000..117c53c --- /dev/null +++ b/lib/Webhooks/WebhookDecoder.php @@ -0,0 +1,50 @@ +setJwksUrl($jwksUrl); + $payload = Utils::parseJWT($token, $jwksUrl); + + return is_array($payload) ? $payload : null; + } catch (\Throwable $e) { + return null; + } + } + +} diff --git a/lib/Webhooks/WebhookEventType.php b/lib/Webhooks/WebhookEventType.php new file mode 100644 index 0000000..e3f9714 --- /dev/null +++ b/lib/Webhooks/WebhookEventType.php @@ -0,0 +1,51 @@ +