From 93859cf846e3946f6d18cf33487c2727442bac27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Sun, 14 Aug 2022 20:59:31 +0200 Subject: [PATCH 01/19] Detector: Fix default Production mode --- src/Detector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Detector.php b/src/Detector.php index 1af21ab..f19f3ec 100644 --- a/src/Detector.php +++ b/src/Detector.php @@ -257,7 +257,7 @@ public static function detect( public static function detectProductionMode( int $mode = self::MODE_SIMPLE, ?string $tempDir = null, - ?bool $default = false + ?bool $default = true ): ?bool { if (is_bool($default)) { $default = !$default; From a4fbd48c6ce7bfd4da809b6ea67a779beda3c516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Sun, 14 Aug 2022 21:05:27 +0200 Subject: [PATCH 02/19] Detector: Rename detectProductionMode method --- .phpstorm.meta.php | 2 +- src/Detector.php | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 29dc04a..7d0b20d 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -29,7 +29,7 @@ ); expectedArguments( - \Redbitcz\DebugMode\Detector::detectProductionMode(), + \Redbitcz\DebugMode\Detector::detectProduction(), 0, \Redbitcz\DebugMode\Detector::MODE_FULL, \Redbitcz\DebugMode\Detector::MODE_SIMPLE, diff --git a/src/Detector.php b/src/Detector.php index f19f3ec..b2c2b28 100644 --- a/src/Detector.php +++ b/src/Detector.php @@ -254,7 +254,7 @@ public static function detect( * @param string|null $tempDir Path to temp directory. Optional, but required when Enabler mode is enabled * @param bool|null $default Default value when no method matches */ - public static function detectProductionMode( + public static function detectProduction( int $mode = self::MODE_SIMPLE, ?string $tempDir = null, ?bool $default = true @@ -271,4 +271,16 @@ public static function detectProductionMode( return $result; } + + /** + * @deprecated Use `detectProduction()` + * @see self::detectProduction() + */ + public static function detectProductionMode( + int $mode = self::MODE_SIMPLE, + ?string $tempDir = null, + ?bool $default = false + ): ?bool { + return self::detectProduction($mode, $tempDir, $default); + } } From a570442c45c4bf26161e358423a0a8790e07f9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Sun, 14 Aug 2022 21:05:55 +0200 Subject: [PATCH 03/19] Tests: Improve performance (fix) --- tests/DetectorTest.php | 2 +- tests/Plugin/SignUrlTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/DetectorTest.php b/tests/DetectorTest.php index 14ca30f..ab3bd58 100644 --- a/tests/DetectorTest.php +++ b/tests/DetectorTest.php @@ -2,6 +2,7 @@ /** * The MIT License (MIT) * Copyright (c) 2022 Redbit s.r.o., Jakub Bouček + * @testCase */ declare(strict_types=1); @@ -18,7 +19,6 @@ require __DIR__ . '/bootstrap.php'; -/** @testCase */ class DetectorTest extends TestCase { private const TEMP_DIR = __DIR__ . '/temp/enabler'; diff --git a/tests/Plugin/SignUrlTest.php b/tests/Plugin/SignUrlTest.php index 9fbf941..eca7095 100644 --- a/tests/Plugin/SignUrlTest.php +++ b/tests/Plugin/SignUrlTest.php @@ -2,6 +2,7 @@ /** * The MIT License (MIT) * Copyright (c) 2022 Redbit s.r.o., Jakub Bouček + * @testCase */ declare(strict_types=1); @@ -16,7 +17,6 @@ require __DIR__ . '/../bootstrap.php'; -/** @testCase */ class SignUrlTest extends \Tester\TestCase { private const KEY_HS256 = "zhYiojmp7O3VYQNuW0C5rS0VgFNgoAvuxW4IdS/0tn8"; From 708bee7e7b72baf952f0e604d8c48bfc96af8b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Sun, 14 Aug 2022 21:36:50 +0200 Subject: [PATCH 04/19] Composer: Froze dev packages version --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b08f4dc..fdc4355 100644 --- a/composer.json +++ b/composer.json @@ -21,8 +21,8 @@ }, "require-dev": { "firebase/php-jwt": "^5.0", - "nette/tester": "^2.4", - "phpstan/phpstan": "^0.12.98" + "nette/tester": "2.4.2", + "phpstan/phpstan": "1.8.2" }, "suggest": { "firebase/php-jwt": "Optional, required for SignedUrl plugin" From 8ca3db53d4f9768f65cbbb0ba0b29d0dcd2dc798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Sun, 14 Aug 2022 21:37:05 +0200 Subject: [PATCH 05/19] Composer: Better specify of PHP versions --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fdc4355..1af45a5 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.4", + "php": "~7.4.0||~8.0.0||~8.1.0", "ext-json": "*", "nette/utils": "^3.0" }, From 9926bee5f2d9e91b9c8a5503ab69af292d0cb9a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Sun, 14 Aug 2022 21:37:33 +0200 Subject: [PATCH 06/19] Tests: Resolve analysed bugs --- tests/DetectorTest.php | 9 ++++++--- tests/Plugin/SignUrlTest.php | 24 ++++++++++++++---------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/tests/DetectorTest.php b/tests/DetectorTest.php index ab3bd58..531cecc 100644 --- a/tests/DetectorTest.php +++ b/tests/DetectorTest.php @@ -1,7 +1,10 @@ getEnabler(); }, InconsistentEnablerModeException::class); @@ -209,7 +212,7 @@ public function testMissingEnabler(): void public function testMissingEnablerShortcut(): void { - Assert::exception(function () { + Assert::exception(static function () { Detector::detect(Detector::MODE_FULL); }, InconsistentEnablerModeException::class); } diff --git a/tests/Plugin/SignUrlTest.php b/tests/Plugin/SignUrlTest.php index eca7095..8941713 100644 --- a/tests/Plugin/SignUrlTest.php +++ b/tests/Plugin/SignUrlTest.php @@ -1,7 +1,10 @@ signUrl($url, 1600000600); }, LogicException::class); } - public function testSignRelativeUrl() + public function testSignRelativeUrl(): void { - Assert::exception(function () { + Assert::exception(static function () { $url = '/login?email=foo@bar.cz'; $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); $plugin->signUrl($url, 1600000600); @@ -179,23 +183,23 @@ public function testVerifyPostRequest(): void $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp($timestamp); JWT::$timestamp = $timestamp; - Assert::exception(function () use ($plugin, $tokenUrl) { + Assert::exception(static function () use ($plugin, $tokenUrl) { $plugin->verifyRequest(false, $tokenUrl, 'POST'); }, SignedUrlVerificationException::class, 'HTTP method doesn\'t match signed HTTP method'); } public function testVerifyInvalidRequest(): void { - Assert::exception(function () { + Assert::exception(static function () { $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); $url = (string)base64_decode('Ly8Eijrg+qawZw=='); $plugin->verifyRequest(false, $url, 'GET'); }, SignedUrlVerificationException::class, 'Url is invalid'); } - public function testVerifyInvalidUrl() + public function testVerifyInvalidUrl(): void { - Assert::exception(function () { + Assert::exception(static function () { $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); $plugin->verifyUrl('https://host.tld/path?query=value'); }, SignedUrlVerificationException::class, 'No token in URL'); @@ -213,7 +217,7 @@ public function testVerifyUrlWithSuffix(): void $tokenUrl .= '&fbclid=123456789'; Assert::exception( - function () use ($timestamp, $tokenUrl) { + static function () use ($timestamp, $tokenUrl) { $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); $plugin->setTimestamp($timestamp); JWT::$timestamp = $timestamp; From 1266a4af3b0b2c54a5f1ec1926c73279ce760fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Wed, 17 Aug 2022 10:20:43 +0200 Subject: [PATCH 07/19] Add Firebase JWT v6 compatibility --- README.md | 6 ++- composer.json | 4 +- phpstan.neon | 10 +++++ src/Plugin/JWT/JWTFirebaseV5.php | 51 +++++++++++++++++++++ src/Plugin/JWT/JWTFirebaseV6.php | 42 +++++++++++++++++ src/Plugin/JWT/JWTImpl.php | 23 ++++++++++ src/Plugin/SignedUrl.php | 40 +++++++++++++---- tests/Plugin/SignUrlTest.php | 77 ++++++++++++++++++++++++++++---- 8 files changed, 234 insertions(+), 19 deletions(-) create mode 100644 src/Plugin/JWT/JWTFirebaseV5.php create mode 100644 src/Plugin/JWT/JWTFirebaseV6.php create mode 100644 src/Plugin/JWT/JWTImpl.php diff --git a/README.md b/README.md index f09d63f..b3acca7 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,16 @@ Package is optimized for invoking in very early lifecycle phase of your App ## Requirements Package requires: -- PHP version at least 7.4 +- PHP version 7.4, 8.0 or 8.1 Enabler requires: - Temporary directory with writable access +SignUrl plugin requires: + +- [Firebase JWT](https://github.com/firebase/php-jwt) v5 or v6 + ## Installation ```shell composer require redbitcz/debug-mode-enabler diff --git a/composer.json b/composer.json index 1af45a5..64e10b3 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "nette/utils": "^3.0" }, "require-dev": { - "firebase/php-jwt": "^5.0", + "firebase/php-jwt": "^5.0||^6.0", "nette/tester": "2.4.2", "phpstan/phpstan": "1.8.2" }, @@ -38,7 +38,7 @@ } }, "scripts": { - "phpstan": "phpstan analyze src -c phpstan.neon --level 8", + "phpstan": "phpstan analyze -c phpstan.neon --level 5", "tester": "tester tests" } } diff --git a/phpstan.neon b/phpstan.neon index 6e9aad6..21dbe2e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,16 @@ parameters: + paths: + - src + excludePaths: + analyse: + - src/Plugin/JWT/* + ignoreErrors: - message: '#Parameter \#3 \$options of function setcookie expects .+#' path: src/Enabler.php count: 2 + - '#.+ has unknown class (OpenSSLAsymmetricKey|OpenSSLCertificate) as its type.#' # PHP 7 compatibility + - '#.+ has invalid type (OpenSSLAsymmetricKey|OpenSSLCertificate).#' # PHP 7 compatibility + + reportUnmatchedIgnoredErrors: false diff --git a/src/Plugin/JWT/JWTFirebaseV5.php b/src/Plugin/JWT/JWTFirebaseV5.php new file mode 100644 index 0000000..50c234b --- /dev/null +++ b/src/Plugin/JWT/JWTFirebaseV5.php @@ -0,0 +1,51 @@ +getParameters(); + + // JWT v5 has second parameter named `$key` + if ($params[1]->getName() === 'key') { + return true; + } + + // JWT v5.5.0 already second parameter named `$keyOrKeyArray`, detect by third param (future compatibility) + return $params[1]->getName() === 'keyOrKeyArray' + && isset($params[2]) + && $params[2]->getName() === 'allowed_algs'; + } + + public function decode(string $jwt, $key, string $alg): stdClass + { + return JWT::decode($jwt, $key, [$alg]); + } + + public function encode(array $payload, $key, string $alg): string + { + return JWT::encode($payload, $key, $alg); + } + + public function setTimestamp(?int $timestamp): void + { + JWT::$timestamp = $timestamp; + } +} diff --git a/src/Plugin/JWT/JWTFirebaseV6.php b/src/Plugin/JWT/JWTFirebaseV6.php new file mode 100644 index 0000000..b243b26 --- /dev/null +++ b/src/Plugin/JWT/JWTFirebaseV6.php @@ -0,0 +1,42 @@ +getParameters(); + + // JWT v6 has always second parameter named `$keyOrKeyArray` + if ($params[1]->getName() !== 'keyOrKeyArray') { + return false; + } + + // JWT v5.5.0 already second parameter named `$keyOrKeyArray`, detect by third param (future compatibility) + return isset($params[2]) === false || $params[2]->getName() !== 'allowed_algs'; + } + + public function decode(string $jwt, $key, string $alg): stdClass + { + return JWT::decode($jwt, new Key($key, $alg)); + } + + +} diff --git a/src/Plugin/JWT/JWTImpl.php b/src/Plugin/JWT/JWTImpl.php new file mode 100644 index 0000000..b65d1ba --- /dev/null +++ b/src/Plugin/JWT/JWTImpl.php @@ -0,0 +1,23 @@ + $impl */ + foreach ([JWTFirebaseV5::class, JWTFirebaseV6::class] as $impl) { + if ($impl::isAvailable()) { + $this->jwt = new $impl; + break; + } + } + + if (isset($this->jwt) === false) { + throw new LogicException(__CLASS__ . ' requires JWT library: firebase/php-jwt version ~5.0 or ~6.0'); } $this->key = $key; @@ -115,7 +134,7 @@ public function getToken( 'val' => $value, ]; - return JWT::encode($payload, $this->key, $this->algorithm); + return $this->jwt->encode($payload, $this->key, $this->algorithm); } public function __invoke(Detector $detector): ?bool @@ -153,7 +172,7 @@ public function verifyRequest(bool $allowRedirect = false, ?string $url = null, $url = $url ?? $this->urlFromGlobal(); $method = $method ?? $_SERVER['REQUEST_METHOD']; - [$allowedMethods, $mode, $value, $expires] = $this->verifyUrl($url); + [$allowedMethods, $mode, $value, $expires] = $this->verifyUrl($url, $allowRedirect); if (in_array(strtolower($method), $allowedMethods, true) === false) { throw new SignedUrlVerificationException('HTTP method doesn\'t match signed HTTP method'); @@ -227,7 +246,7 @@ public function verifyToken(string $token): array { try { /** @var ClaimsSet $payload */ - $payload = JWT::decode($token, $this->key, [$this->algorithm]); + $payload = $this->jwt->decode($token, $this->key, $this->algorithm); } catch (RuntimeException $e) { throw new SignedUrlVerificationException('JWT Token invalid', 0, $e); } @@ -324,6 +343,11 @@ public function setTimestamp(?int $timestamp): void $this->timestamp = $timestamp; } + public function getJwt(): JWTImpl + { + return $this->jwt; + } + protected function sendRedirectResponse(string $canonicalUrl): void { header('Cache-Control: s-maxage=0, max-age=0, must-revalidate', true, 302); diff --git a/tests/Plugin/SignUrlTest.php b/tests/Plugin/SignUrlTest.php index 8941713..eb8a5e7 100644 --- a/tests/Plugin/SignUrlTest.php +++ b/tests/Plugin/SignUrlTest.php @@ -14,6 +14,7 @@ use Firebase\JWT\JWT; use LogicException; +use Redbitcz\DebugMode\Plugin\JWT\JWTFirebaseV6; use Redbitcz\DebugMode\Plugin\SignedUrl; use Redbitcz\DebugMode\Plugin\SignedUrlVerificationException; use Tester\Assert; @@ -25,6 +26,7 @@ class SignUrlTest extends TestCase { private const KEY_HS256 = "zhYiojmp7O3VYQNuW0C5rS0VgFNgoAvuxW4IdS/0tn8"; + public function testSign(): void { $audience = 'test.' . __FUNCTION__; @@ -32,7 +34,19 @@ public function testSign(): void $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp(1600000000); $token = $plugin->signUrl('https://host.tld/path', 1600000600); - $expected = 'https://host.tld/path?_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGgiLCJtZXRoIjpbImdldCJdLCJtb2QiOjAsInZhbCI6MX0.MTZOii4lQ2WCk1UltRx_e9T5vCT7nq8G3kh4D8EXy7s'; + + if ($plugin->getJwt() instanceof JWTFirebaseV6) { + $expected = 'https://host.tld/path?_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVi' + . 'dWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRw' + . 'czovL2hvc3QudGxkL3BhdGgiLCJtZXRoIjpbImdldCJdLCJtb2QiOjAsInZhbCI6MX0.h2TAkamMzGVQkre-F9kaCSmg3irRt9qv' + . '84oUcxj9gv0'; + } else { + $expected = 'https://host.tld/path?_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVi' + . 'dWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRw' + . 'czpcL1wvaG9zdC50bGRcL3BhdGgiLCJtZXRoIjpbImdldCJdLCJtb2QiOjAsInZhbCI6MX0.MTZOii4lQ2WCk1UltRx_e9T5vCT7' + . 'nq8G3kh4D8EXy7s'; + } + Assert::equal($expected, $token); } @@ -43,7 +57,19 @@ public function testSignQuery(): void $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp(1600000000); $token = $plugin->signUrl('https://host.tld/path?query=value', 1600000600); - $expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnblF1ZXJ5IiwiaWF0IjoxNjAwMDAwMDAwLCJleHAiOjE2MDAwMDA2MDAsInN1YiI6Imh0dHBzOlwvXC9ob3N0LnRsZFwvcGF0aD9xdWVyeT12YWx1ZSIsIm1ldGgiOlsiZ2V0Il0sIm1vZCI6MCwidmFsIjoxfQ.RrO7BCmdgldB7OlEIpudBWo8P33xDh-MsNjtZC34CNY'; + + if ($plugin->getJwt() instanceof JWTFirebaseV6) { + $expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5y' + . 'ZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnblF1ZXJ5IiwiaWF0IjoxNjAwMDAwMDAwLCJleHAiOjE2MDAwMDA2' + . 'MDAsInN1YiI6Imh0dHBzOi8vaG9zdC50bGQvcGF0aD9xdWVyeT12YWx1ZSIsIm1ldGgiOlsiZ2V0Il0sIm1vZCI6MCwidmFsIjox' + . 'fQ.UXB2AIKChgunDzoY7hcWNA7vg7j6sf3VvOWFw0OKz8k'; + } else { + $expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5y' + . 'ZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnblF1ZXJ5IiwiaWF0IjoxNjAwMDAwMDAwLCJleHAiOjE2MDAwMDA2' + . 'MDAsInN1YiI6Imh0dHBzOlwvXC9ob3N0LnRsZFwvcGF0aD9xdWVyeT12YWx1ZSIsIm1ldGgiOlsiZ2V0Il0sIm1vZCI6MCwidmFs' + . 'IjoxfQ.RrO7BCmdgldB7OlEIpudBWo8P33xDh-MsNjtZC34CNY'; + } + Assert::equal($expected, $token); } @@ -54,7 +80,19 @@ public function testSignFragment(): void $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp(1600000000); $token = $plugin->signUrl('https://host.tld/path?query=value#fragment', 1600000600); - $expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbkZyYWdtZW50IiwiaWF0IjoxNjAwMDAwMDAwLCJleHAiOjE2MDAwMDA2MDAsInN1YiI6Imh0dHBzOlwvXC9ob3N0LnRsZFwvcGF0aD9xdWVyeT12YWx1ZSIsIm1ldGgiOlsiZ2V0Il0sIm1vZCI6MCwidmFsIjoxfQ.9oIORBXW-hW8vTPdJglEdEMm19nwAvw2wLAxqWvFh3Y#fragment'; + + if ($plugin->getJwt() instanceof JWTFirebaseV6) { + $expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5y' + . 'ZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbkZyYWdtZW50IiwiaWF0IjoxNjAwMDAwMDAwLCJleHAiOjE2MDAw' + . 'MDA2MDAsInN1YiI6Imh0dHBzOi8vaG9zdC50bGQvcGF0aD9xdWVyeT12YWx1ZSIsIm1ldGgiOlsiZ2V0Il0sIm1vZCI6MCwidmFs' + . 'IjoxfQ.-Aww363VPD0aSi5QK1JH2v_4yFU5DX5aRvbsxqtcJSg#fragment'; + } else { + $expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5y' + . 'ZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbkZyYWdtZW50IiwiaWF0IjoxNjAwMDAwMDAwLCJleHAiOjE2MDAw' + . 'MDA2MDAsInN1YiI6Imh0dHBzOlwvXC9ob3N0LnRsZFwvcGF0aD9xdWVyeT12YWx1ZSIsIm1ldGgiOlsiZ2V0Il0sIm1vZCI6MCwi' + . 'dmFsIjoxfQ.9oIORBXW-hW8vTPdJglEdEMm19nwAvw2wLAxqWvFh3Y#fragment'; + } + Assert::equal($expected, $token); } @@ -71,7 +109,19 @@ public function testGetToken(): void SignedUrl::MODE_REQUEST, SignedUrl::VALUE_ENABLE ); - $expected = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0R2V0VG9rZW4iLCJpYXQiOjE2MDAwMDAwMDAsImV4cCI6MTYwMDAwMDYwMCwic3ViIjoiaHR0cHM6XC9cL2hvc3QudGxkXC9wYXRoP3F1ZXJ5PXZhbHVlIiwibWV0aCI6WyJnZXQiXSwibW9kIjowLCJ2YWwiOjF9.I6tEfFneSxuY9qAjRf5esYFPonChbliZqGoijtv2iHw'; + + if ($plugin->getJwt() instanceof JWTFirebaseV6) { + $expected = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50Z' + . 'XN0R2V0VG9rZW4iLCJpYXQiOjE2MDAwMDAwMDAsImV4cCI6MTYwMDAwMDYwMCwic3ViIjoiaHR0cHM6Ly9ob3N0LnRsZC9wYXRoP' + . '3F1ZXJ5PXZhbHVlIiwibWV0aCI6WyJnZXQiXSwibW9kIjowLCJ2YWwiOjF9.LrE8DVuvXiP4u3cHXiSABIOXI4WlHFBxf2g-DRYW' + . 'xNQ'; + } else { + $expected = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50Z' + . 'XN0R2V0VG9rZW4iLCJpYXQiOjE2MDAwMDAwMDAsImV4cCI6MTYwMDAwMDYwMCwic3ViIjoiaHR0cHM6XC9cL2hvc3QudGxkXC9wY' + . 'XRoP3F1ZXJ5PXZhbHVlIiwibWV0aCI6WyJnZXQiXSwibW9kIjowLCJ2YWwiOjF9.I6tEfFneSxuY9qAjRf5esYFPonChbliZqGoi' + . 'jtv2iHw'; + } + Assert::equal($expected, $token); } @@ -231,14 +281,20 @@ static function () use ($timestamp, $tokenUrl) { public function testVerifyUrlWithSuffixRedirect(): void { $timestamp = 1600000000; - $tokenUrl = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c' + $tokenUrl = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRi' + . 'aXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJo' + . 'dHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDo' + . 'UhOfsZ4m16Q3hjtVFJep_t_qoQ5c' . '&fbclid=123456789'; // Mock plugin without redirect $plugin = new class(self::KEY_HS256, 'HS256', 'test.testSign') extends SignedUrl { protected function sendRedirectResponse(string $canonicalUrl): void { - $expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c'; + $expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJj' + . 'ei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAw' + . 'NjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2' + . 'YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c'; Assert::equal($expected, $canonicalUrl); } }; @@ -252,7 +308,9 @@ public function testVerifyUrlWithSuffixRedirectFragment(): void { $timestamp = 1600000000; $tokenUrl = 'https://host.tld/path?query=value' - . '&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c' + . '&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN' + . '0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk' + . '9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c' . '&fbclid=123456789' . '#hash'; @@ -261,7 +319,10 @@ public function testVerifyUrlWithSuffixRedirectFragment(): void protected function sendRedirectResponse(string $canonicalUrl): void { $expected = 'https://host.tld/path?query=value' - . '&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c' + . '&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGV' + . 'zdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGR' + . 'cL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjt' + . 'VFJep_t_qoQ5c' . '#hash'; Assert::equal($expected, $canonicalUrl); } From 4efe7cf2edc38f7839c1f2a92a2c5550b58d8639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Wed, 17 Aug 2022 10:49:55 +0200 Subject: [PATCH 08/19] Github actions - add php 8.1, add lowest deps test --- .github/workflows/code_analysis.yaml | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml index 0dc13ac..94ed923 100644 --- a/.github/workflows/code_analysis.yaml +++ b/.github/workflows/code_analysis.yaml @@ -23,10 +23,19 @@ jobs: php: - "7.4" - "8.0" + - "8.1" - name: ${{ matrix.actions.name }} on PHP ${{ matrix.php }} - runs-on: ubuntu-latest + deps: + - name: Lowest deps + key: lowest + arg: --prefer-lowest + + - name: Current deps + key: current + arg: '' + name: ${{ matrix.actions.name }} on PHP ${{ matrix.php }} with ${{ matrix.deps.name }} + runs-on: ubuntu-latest steps: - name: Checkout @@ -53,18 +62,20 @@ jobs: with: path: | ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-data-${{ hashFiles('composer.json') }}-php-${{ matrix.php }} + key: ${{ runner.os }}-composer-data-${{ hashFiles('composer.json') }}-php-${{ matrix.php }}-${{ matrix.deps.name }} - uses: actions/cache@v2 with: path: | **/composer.lock - key: ${{ runner.os }}-composer-lock-${{ hashFiles('composer.json') }}-php-${{ matrix.php }} + key: ${{ runner.os }}-composer-lock-${{ hashFiles('composer.json') }}-php-${{ matrix.php }}-${{ matrix.deps.name }} - name: Install Composer - run: composer install --no-progress + run: composer update --no-progress ${{ matrix.deps.arg }} - - run: ${{ matrix.actions.run }} + - name: Run job + if: ${{ matrix.deps.key != 'lowest' && matrix.php != '8.1 }} + run: ${{ matrix.actions.run }} From ec9ca51b3b10332660eab52d2f8e620ce5b72c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Wed, 17 Aug 2022 15:30:24 +0200 Subject: [PATCH 09/19] Move credentials to JWTImpl --- src/Plugin/JWT/JWTFirebaseV5.php | 24 ++++++++++++++--- src/Plugin/JWT/JWTFirebaseV6.php | 6 +++-- src/Plugin/JWT/JWTImpl.php | 4 +-- src/Plugin/SignedUrl.php | 30 ++++++++++------------ tests/Plugin/SignUrlTest.php | 44 ++++++++++++++++---------------- 5 files changed, 62 insertions(+), 46 deletions(-) diff --git a/src/Plugin/JWT/JWTFirebaseV5.php b/src/Plugin/JWT/JWTFirebaseV5.php index 50c234b..716d1b9 100644 --- a/src/Plugin/JWT/JWTFirebaseV5.php +++ b/src/Plugin/JWT/JWTFirebaseV5.php @@ -3,6 +3,8 @@ /** * The MIT License (MIT) * Copyright (c) 2022 Redbit s.r.o., Jakub Bouček + * + * @noinspection PhpUndefinedClassInspection OpenSSL only optional dependency */ declare(strict_types=1); @@ -15,6 +17,20 @@ class JWTFirebaseV5 implements JWTImpl { + /** @var OpenSSLAsymmetricKey|OpenSSLCertificate|resource|string */ + protected $key; + protected string $algorithm; + + /** + * @param OpenSSLAsymmetricKey|OpenSSLCertificate|resource|string $key + * @param string $algorithm + */ + public function __construct($key, string $algorithm = 'HS256') + { + $this->key = $key; + $this->algorithm = $algorithm; + } + public static function isAvailable(): bool { if (class_exists(JWT::class) === false) { @@ -34,14 +50,14 @@ public static function isAvailable(): bool && $params[2]->getName() === 'allowed_algs'; } - public function decode(string $jwt, $key, string $alg): stdClass + public function decode(string $jwt): stdClass { - return JWT::decode($jwt, $key, [$alg]); + return JWT::decode($jwt, $this->key, [$this->algorithm]); } - public function encode(array $payload, $key, string $alg): string + public function encode(array $payload): string { - return JWT::encode($payload, $key, $alg); + return JWT::encode($payload, $this->key, $this->algorithm); } public function setTimestamp(?int $timestamp): void diff --git a/src/Plugin/JWT/JWTFirebaseV6.php b/src/Plugin/JWT/JWTFirebaseV6.php index b243b26..b8a8100 100644 --- a/src/Plugin/JWT/JWTFirebaseV6.php +++ b/src/Plugin/JWT/JWTFirebaseV6.php @@ -3,6 +3,8 @@ /** * The MIT License (MIT) * Copyright (c) 2022 Redbit s.r.o., Jakub Bouček + * + * @noinspection PhpUndefinedClassInspection Library support JWT 5.0 */ declare(strict_types=1); @@ -33,9 +35,9 @@ public static function isAvailable(): bool return isset($params[2]) === false || $params[2]->getName() !== 'allowed_algs'; } - public function decode(string $jwt, $key, string $alg): stdClass + public function decode(string $jwt): stdClass { - return JWT::decode($jwt, new Key($key, $alg)); + return JWT::decode($jwt, new Key($this->key, $this->algorithm)); } diff --git a/src/Plugin/JWT/JWTImpl.php b/src/Plugin/JWT/JWTImpl.php index b65d1ba..eea0c21 100644 --- a/src/Plugin/JWT/JWTImpl.php +++ b/src/Plugin/JWT/JWTImpl.php @@ -15,9 +15,9 @@ interface JWTImpl { public static function isAvailable(): bool; - public function decode(string $jwt, $key, string $alg): stdClass; + public function decode(string $jwt): stdClass; - public function encode(array $payload, $key, string $alg): string; + public function encode(array $payload): string; public function setTimestamp(?int $timestamp): void; } diff --git a/src/Plugin/SignedUrl.php b/src/Plugin/SignedUrl.php index 13e6f43..988f112 100644 --- a/src/Plugin/SignedUrl.php +++ b/src/Plugin/SignedUrl.php @@ -45,37 +45,35 @@ class SignedUrl implements Plugin private const URL_QUERY_TOKEN_KEY = '_debug'; private const ISSUER_ID = 'cz.redbit.debug.url'; - /** @var resource|string|OpenSSLAsymmetricKey|OpenSSLCertificate */ - private $key; - private string $algorithm; private ?string $audience; private ?int $timestamp; private JWTImpl $jwt; + /** + * @param string|null $audience Recipient for which the JWT is intended + */ + public function __construct(JWTImpl $jwt, ?string $audience = null) + { + $this->jwt = $jwt; + $this->audience = $audience; + } + /** * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The key. * @param string $algorithm Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' - * @param string|null $audience Recipient for which the JWT is intended * @noinspection PhpRedundantVariableDocTypeInspection */ - public function __construct($key, string $algorithm = 'HS256', ?string $audience = null) + public static function create($key, string $algorithm = 'HS256', ?string $audience = null): self { /** @var class-string $impl */ foreach ([JWTFirebaseV5::class, JWTFirebaseV6::class] as $impl) { if ($impl::isAvailable()) { - $this->jwt = new $impl; - break; + return new self(new $impl($key, $algorithm), $audience); } } - if (isset($this->jwt) === false) { - throw new LogicException(__CLASS__ . ' requires JWT library: firebase/php-jwt version ~5.0 or ~6.0'); - } - - $this->key = $key; - $this->algorithm = $algorithm; - $this->audience = $audience; + throw new LogicException(__CLASS__ . ' requires JWT library: firebase/php-jwt version ~5.0 or ~6.0'); } /** @@ -134,7 +132,7 @@ public function getToken( 'val' => $value, ]; - return $this->jwt->encode($payload, $this->key, $this->algorithm); + return $this->jwt->encode($payload); } public function __invoke(Detector $detector): ?bool @@ -246,7 +244,7 @@ public function verifyToken(string $token): array { try { /** @var ClaimsSet $payload */ - $payload = $this->jwt->decode($token, $this->key, $this->algorithm); + $payload = $this->jwt->decode($token); } catch (RuntimeException $e) { throw new SignedUrlVerificationException('JWT Token invalid', 0, $e); } diff --git a/tests/Plugin/SignUrlTest.php b/tests/Plugin/SignUrlTest.php index eb8a5e7..fee9e7b 100644 --- a/tests/Plugin/SignUrlTest.php +++ b/tests/Plugin/SignUrlTest.php @@ -31,7 +31,7 @@ public function testSign(): void { $audience = 'test.' . __FUNCTION__; - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp(1600000000); $token = $plugin->signUrl('https://host.tld/path', 1600000600); @@ -54,7 +54,7 @@ public function testSignQuery(): void { $audience = 'test.' . __FUNCTION__; - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp(1600000000); $token = $plugin->signUrl('https://host.tld/path?query=value', 1600000600); @@ -77,7 +77,7 @@ public function testSignFragment(): void { $audience = 'test.' . __FUNCTION__; - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp(1600000000); $token = $plugin->signUrl('https://host.tld/path?query=value#fragment', 1600000600); @@ -100,7 +100,7 @@ public function testGetToken(): void { $audience = 'test.' . __FUNCTION__; - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp(1600000000); $token = $plugin->getToken( 'https://host.tld/path?query=value', @@ -130,7 +130,7 @@ public function testVerifyToken(): void $audience = 'test.' . __FUNCTION__; $timestamp = 1600000000; - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp($timestamp); $token = $plugin->getToken( 'https://host.tld/path?query=value', @@ -140,7 +140,7 @@ public function testVerifyToken(): void SignedUrl::VALUE_ENABLE ); - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp($timestamp); JWT::$timestamp = $timestamp; $parsed = $plugin->verifyToken($token); @@ -154,11 +154,11 @@ public function testVerifyUrl(): void $timestamp = 1600000000; $url = 'https://host.tld/path'; - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp($timestamp); $tokenUrl = $plugin->signUrl($url, 1600000600); - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp($timestamp); JWT::$timestamp = $timestamp; $parsed = $plugin->verifyUrl($tokenUrl); @@ -172,11 +172,11 @@ public function testVerifyUrlQuery(): void $timestamp = 1600000000; $url = 'https://host.tld/path?query=value'; - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp($timestamp); $tokenUrl = $plugin->signUrl($url, 1600000600); - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp($timestamp); JWT::$timestamp = $timestamp; $parsed = $plugin->verifyUrl($tokenUrl); @@ -190,11 +190,11 @@ public function testVerifyRequest(): void $timestamp = 1600000000; $url = 'https://host.tld/path?query=value'; - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp($timestamp); $tokenUrl = $plugin->signUrl($url, 1600000600); - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp($timestamp); JWT::$timestamp = $timestamp; $parsed = $plugin->verifyRequest(false, $tokenUrl, 'GET'); @@ -206,7 +206,7 @@ public function testSignInvalidUrl(): void { Assert::exception(static function () { $url = (string)base64_decode('Ly8Eijrg+qawZw=='); - $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256'); $plugin->signUrl($url, 1600000600); }, LogicException::class); } @@ -215,7 +215,7 @@ public function testSignRelativeUrl(): void { Assert::exception(static function () { $url = '/login?email=foo@bar.cz'; - $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256'); $plugin->signUrl($url, 1600000600); }, LogicException::class); } @@ -226,11 +226,11 @@ public function testVerifyPostRequest(): void $timestamp = 1600000000; $url = 'https://host.tld/path?query=value'; - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp($timestamp); $tokenUrl = $plugin->signUrl($url, 1600000600); - $plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256', $audience); $plugin->setTimestamp($timestamp); JWT::$timestamp = $timestamp; Assert::exception(static function () use ($plugin, $tokenUrl) { @@ -241,7 +241,7 @@ public function testVerifyPostRequest(): void public function testVerifyInvalidRequest(): void { Assert::exception(static function () { - $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256'); $url = (string)base64_decode('Ly8Eijrg+qawZw=='); $plugin->verifyRequest(false, $url, 'GET'); }, SignedUrlVerificationException::class, 'Url is invalid'); @@ -250,7 +250,7 @@ public function testVerifyInvalidRequest(): void public function testVerifyInvalidUrl(): void { Assert::exception(static function () { - $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256'); $plugin->verifyUrl('https://host.tld/path?query=value'); }, SignedUrlVerificationException::class, 'No token in URL'); } @@ -260,7 +260,7 @@ public function testVerifyUrlWithSuffix(): void $timestamp = 1600000000; $url = 'https://host.tld/path?query=value'; - $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256'); $plugin->setTimestamp($timestamp); $tokenUrl = $plugin->signUrl($url, 1600000600); @@ -268,7 +268,7 @@ public function testVerifyUrlWithSuffix(): void Assert::exception( static function () use ($timestamp, $tokenUrl) { - $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256'); $plugin->setTimestamp($timestamp); JWT::$timestamp = $timestamp; $plugin->verifyUrl($tokenUrl); @@ -288,7 +288,7 @@ public function testVerifyUrlWithSuffixRedirect(): void . '&fbclid=123456789'; // Mock plugin without redirect - $plugin = new class(self::KEY_HS256, 'HS256', 'test.testSign') extends SignedUrl { + $plugin = new class(SignedUrl::create(self::KEY_HS256)->getJwt(), 'test.testSign') extends SignedUrl { protected function sendRedirectResponse(string $canonicalUrl): void { $expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJj' @@ -315,7 +315,7 @@ public function testVerifyUrlWithSuffixRedirectFragment(): void . '#hash'; // Mock plugin without redirect - $plugin = new class(self::KEY_HS256, 'HS256', 'test.testSign') extends SignedUrl { + $plugin = new class(SignedUrl::create(self::KEY_HS256)->getJwt(), 'test.testSign') extends SignedUrl { protected function sendRedirectResponse(string $canonicalUrl): void { $expected = 'https://host.tld/path?query=value' From 7e4f24736ea9c9a7a33d3b0e3a7d3849bf7b5cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Fri, 3 Feb 2023 14:28:35 +0100 Subject: [PATCH 10/19] Composer: Update PhpStan & normalize composer.json --- composer.json | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 64e10b3..d15b7d5 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,12 @@ { "name": "redbitcz/debug-mode-enabler", "description": "Debug mode enabler - safe and clean way to manage Debug Mode in your App", - "keywords": ["debug"], - "license": ["MIT"], - "homepage": "https://github.com/redbitcz/php-debug-mode-enabler", + "license": [ + "MIT" + ], + "keywords": [ + "debug" + ], "authors": [ { "name": "Redbit s.r.o.", @@ -14,15 +17,16 @@ "homepage": "https://www.jakub-boucek.cz/" } ], + "homepage": "https://github.com/redbitcz/php-debug-mode-enabler", "require": { - "php": "~7.4.0||~8.0.0||~8.1.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0", "ext-json": "*", "nette/utils": "^3.0" }, "require-dev": { - "firebase/php-jwt": "^5.0||^6.0", + "firebase/php-jwt": "^5.0 || ^6.0", "nette/tester": "2.4.2", - "phpstan/phpstan": "1.8.2" + "phpstan/phpstan": "1.9.14" }, "suggest": { "firebase/php-jwt": "Optional, required for SignedUrl plugin" @@ -37,6 +41,9 @@ "Redbitcz\\DebugModeTests\\": "tests/" } }, + "config": { + "sort-packages": true + }, "scripts": { "phpstan": "phpstan analyze -c phpstan.neon --level 5", "tester": "tester tests" From 69d7ec6da882e923a30a9f456958bf29b2acc140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Fri, 3 Feb 2023 14:29:07 +0100 Subject: [PATCH 11/19] Update SignedUrl & improve CI to exactly test JWT v5 & v6 --- .github/workflows/code_analysis.yaml | 24 ++++++++++++------------ src/Plugin/SignedUrl.php | 3 --- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml index 94ed923..133f3f5 100644 --- a/.github/workflows/code_analysis.yaml +++ b/.github/workflows/code_analysis.yaml @@ -24,17 +24,18 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" - deps: - - name: Lowest deps - key: lowest - arg: --prefer-lowest + jwt: + - name: JWT 5 + key: jtw5 + arg: '"firebase/php-jwt:^5.0"' - - name: Current deps - key: current - arg: '' + - name: JWT 6 + key: jwt6 + arg: '"firebase/php-jwt:^6.0"' - name: ${{ matrix.actions.name }} on PHP ${{ matrix.php }} with ${{ matrix.deps.name }} + name: ${{ matrix.actions.name }} on PHP ${{ matrix.php }} with ${{ matrix.jwt.name }} runs-on: ubuntu-latest steps: @@ -62,20 +63,19 @@ jobs: with: path: | ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-data-${{ hashFiles('composer.json') }}-php-${{ matrix.php }}-${{ matrix.deps.name }} + key: ${{ runner.os }}-composer-data-${{ hashFiles('composer.json') }}-php-${{ matrix.php }}-${{ matrix.jwt.name }} - uses: actions/cache@v2 with: path: | **/composer.lock - key: ${{ runner.os }}-composer-lock-${{ hashFiles('composer.json') }}-php-${{ matrix.php }}-${{ matrix.deps.name }} + key: ${{ runner.os }}-composer-lock-${{ hashFiles('composer.json') }}-php-${{ matrix.php }}-${{ matrix.jwt.key }} - name: Install Composer - run: composer update --no-progress ${{ matrix.deps.arg }} + run: composer update --no-progress --with ${{ matrix.jwt.arg }} - name: Run job - if: ${{ matrix.deps.key != 'lowest' && matrix.php != '8.1 }} run: ${{ matrix.actions.run }} diff --git a/src/Plugin/SignedUrl.php b/src/Plugin/SignedUrl.php index 988f112..30a6280 100644 --- a/src/Plugin/SignedUrl.php +++ b/src/Plugin/SignedUrl.php @@ -25,7 +25,6 @@ /** * @phpstan-type ParsedUrl array{'scheme'?: string, 'host'?: string, 'port'?: int, 'user'?: string, 'pass'?: string, 'path'?: string, 'query'?: string, 'fragment'?: string} - * @phpstan-type ClaimsSet array{'iss': string, 'aud': string|null, 'iat': int, 'exp': int, 'sub': string, 'meth': array, 'mod': int, 'val': int} */ class SignedUrl implements Plugin { @@ -243,7 +242,6 @@ public function verifyUrl(string $url, bool $allowRedirect = false): array public function verifyToken(string $token): array { try { - /** @var ClaimsSet $payload */ $payload = $this->jwt->decode($token); } catch (RuntimeException $e) { throw new SignedUrlVerificationException('JWT Token invalid', 0, $e); @@ -364,7 +362,6 @@ protected function normalizeUrl(array $url): array { $url['path'] = ($url['path'] ?? '') === '' ? '/' : ($url['path'] ?? ''); unset($url['fragment']); - /** @var ParsedUrl $url (bypass PhpStan bug) */ return $url; } } From 4126cddda8f76f856859267a28bb56d6b1380d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Fri, 3 Feb 2023 14:34:57 +0100 Subject: [PATCH 12/19] Composer: Update Netter Tester 2.4.3 (PHP 8.2 compat) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d15b7d5..5a0823a 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ }, "require-dev": { "firebase/php-jwt": "^5.0 || ^6.0", - "nette/tester": "2.4.2", + "nette/tester": "2.4.3", "phpstan/phpstan": "1.9.14" }, "suggest": { From 47f5ad6eaaa7783a2d039362731017088502f65d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Fri, 3 Feb 2023 14:36:29 +0100 Subject: [PATCH 13/19] Composer: Add PHP 8.2 to accepted --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5a0823a..a82bc4d 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ ], "homepage": "https://github.com/redbitcz/php-debug-mode-enabler", "require": { - "php": "~7.4.0 || ~8.0.0 || ~8.1.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0", "ext-json": "*", "nette/utils": "^3.0" }, From 30df4c586447bc02378ab1d23db01e80b72acee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Fri, 3 Feb 2023 14:53:31 +0100 Subject: [PATCH 14/19] PhpStan: Ignore false-positive --- phpstan.neon | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 21dbe2e..55b860b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,11 +6,21 @@ parameters: - src/Plugin/JWT/* ignoreErrors: - - + - # Too variants of parameters signature between PHP versions message: '#Parameter \#3 \$options of function setcookie expects .+#' + reportUnmatched: false path: src/Enabler.php count: 2 - - '#.+ has unknown class (OpenSSLAsymmetricKey|OpenSSLCertificate) as its type.#' # PHP 7 compatibility - - '#.+ has invalid type (OpenSSLAsymmetricKey|OpenSSLCertificate).#' # PHP 7 compatibility - reportUnmatchedIgnoredErrors: false + - # Weird bug by PhpStan - the `path` fiealt is still nullable + message: '#Offset ''path'' on array.+ on left side of \?\? always exists and is not nullable\.#' + path: src/Plugin/SignedUrl.php + count: 1 + + - + message: '#.+ has unknown class (OpenSSLAsymmetricKey|OpenSSLCertificate) as its type.#' # PHP 7 compatibility + reportUnmatched: false + + - + message: '#.+ has invalid type (OpenSSLAsymmetricKey|OpenSSLCertificate).#' # PHP 7 compatibility + reportUnmatched: false From 4f79bc310c26e0a9435f9b4f33ed6919bd5a4f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Fri, 3 Feb 2023 14:56:51 +0100 Subject: [PATCH 15/19] Update Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b3acca7..7da273f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Package is optimized for invoking in very early lifecycle phase of your App ## Requirements Package requires: -- PHP version 7.4, 8.0 or 8.1 +- PHP version 7.4, 8.0, 8.1 or 8.2 Enabler requires: @@ -169,7 +169,7 @@ $detector->isDebugMode(); // <---- this invoke all Plugins `SignUrl` plugin provide secure way to share link with activated Debug Mode. ```php -$plugin = new \Redbitcz\DebugMode\Plugin\SignedUrl('secretkey', 'HS256', 'https://myapp.cz'); +$plugin = \Redbitcz\DebugMode\Plugin\SignedUrl::create('secretkey', 'HS256', 'https://myapp.cz'); $detector->appendPlugin($plugin); $signedUrl = $plugin->signUrl('https://myapp.cz/failingPage', '+1 hour'); From d5a399fd5de7c33ddd2cd696d877aa95550feeea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Fri, 3 Feb 2023 15:13:50 +0100 Subject: [PATCH 16/19] Composer: improve suggest section --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a82bc4d..9052025 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpstan/phpstan": "1.9.14" }, "suggest": { - "firebase/php-jwt": "Optional, required for SignedUrl plugin" + "firebase/php-jwt": "Optional, required for SignedUrl plugin, compatible with version 5.x and 6.x" }, "autoload": { "psr-4": { From b95a977e6d10b1dd228c350130ee3e430ac89014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Fri, 3 Feb 2023 15:24:39 +0100 Subject: [PATCH 17/19] Licence: Update year --- .phpstorm.meta.php | 5 ++--- LICENSE | 2 +- src/Detector.php | 2 +- src/Enabler.php | 2 +- src/InconsistentEnablerModeException.php | 2 +- src/Plugin/JWT/JWTFirebaseV5.php | 2 +- src/Plugin/JWT/JWTFirebaseV6.php | 2 +- src/Plugin/JWT/JWTImpl.php | 2 +- src/Plugin/Plugin.php | 2 +- src/Plugin/SignedUrl.php | 2 +- src/Plugin/SignedUrlVerificationException.php | 2 +- tests/DetectorTest.php | 2 +- tests/Plugin/SignUrlTest.php | 2 +- tests/bootstrap.php | 2 +- 14 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 7d0b20d..2e7287f 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -1,11 +1,10 @@ Date: Fri, 3 Feb 2023 15:37:10 +0100 Subject: [PATCH 18/19] Remove deprecations from v3 --- src/Detector.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Detector.php b/src/Detector.php index 5f3d591..14841fb 100644 --- a/src/Detector.php +++ b/src/Detector.php @@ -271,16 +271,4 @@ public static function detectProduction( return $result; } - - /** - * @deprecated Use `detectProduction()` - * @see self::detectProduction() - */ - public static function detectProductionMode( - int $mode = self::MODE_SIMPLE, - ?string $tempDir = null, - ?bool $default = false - ): ?bool { - return self::detectProduction($mode, $tempDir, $default); - } } From bb05dbc88282e24836c6e7c5cf44f2f95c4196e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Fri, 3 Feb 2023 15:38:17 +0100 Subject: [PATCH 19/19] CI: Check all branches --- .github/workflows/code_analysis.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml index 133f3f5..6934047 100644 --- a/.github/workflows/code_analysis.yaml +++ b/.github/workflows/code_analysis.yaml @@ -4,8 +4,6 @@ name: Code Analysis on: pull_request: push: - branches: - - master jobs: