diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml index 0dc13ac..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: @@ -23,10 +21,20 @@ jobs: php: - "7.4" - "8.0" + - "8.1" + - "8.2" - name: ${{ matrix.actions.name }} on PHP ${{ matrix.php }} - runs-on: ubuntu-latest + jwt: + - name: JWT 5 + key: jtw5 + arg: '"firebase/php-jwt:^5.0"' + + - name: JWT 6 + key: jwt6 + arg: '"firebase/php-jwt:^6.0"' + name: ${{ matrix.actions.name }} on PHP ${{ matrix.php }} with ${{ matrix.jwt.name }} + runs-on: ubuntu-latest steps: - name: Checkout @@ -53,18 +61,19 @@ 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.jwt.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.jwt.key }} - name: Install Composer - run: composer install --no-progress + run: composer update --no-progress --with ${{ matrix.jwt.arg }} - - run: ${{ matrix.actions.run }} + - name: Run job + run: ${{ matrix.actions.run }} diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 29dc04a..2e7287f 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -1,11 +1,10 @@ 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'); diff --git a/composer.json b/composer.json index b08f4dc..9052025 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,18 +17,19 @@ "homepage": "https://www.jakub-boucek.cz/" } ], + "homepage": "https://github.com/redbitcz/php-debug-mode-enabler", "require": { - "php": ">=7.4", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0", "ext-json": "*", "nette/utils": "^3.0" }, "require-dev": { - "firebase/php-jwt": "^5.0", - "nette/tester": "^2.4", - "phpstan/phpstan": "^0.12.98" + "firebase/php-jwt": "^5.0 || ^6.0", + "nette/tester": "2.4.3", + "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": { @@ -37,8 +41,11 @@ "Redbitcz\\DebugModeTests\\": "tests/" } }, + "config": { + "sort-packages": true + }, "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..55b860b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,26 @@ parameters: + paths: + - src + excludePaths: + analyse: + - 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 + + - # 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 diff --git a/src/Detector.php b/src/Detector.php index 1af21ab..14841fb 100644 --- a/src/Detector.php +++ b/src/Detector.php @@ -1,7 +1,7 @@ key = $key; + $this->algorithm = $algorithm; + } + + public static function isAvailable(): bool + { + if (class_exists(JWT::class) === false) { + return false; + } + + $params = (new ReflectionMethod(JWT::class, 'decode'))->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): stdClass + { + return JWT::decode($jwt, $this->key, [$this->algorithm]); + } + + public function encode(array $payload): string + { + return JWT::encode($payload, $this->key, $this->algorithm); + } + + 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..da14323 --- /dev/null +++ b/src/Plugin/JWT/JWTFirebaseV6.php @@ -0,0 +1,44 @@ +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): stdClass + { + return JWT::decode($jwt, new Key($this->key, $this->algorithm)); + } + + +} diff --git a/src/Plugin/JWT/JWTImpl.php b/src/Plugin/JWT/JWTImpl.php new file mode 100644 index 0000000..ca9f25a --- /dev/null +++ b/src/Plugin/JWT/JWTImpl.php @@ -0,0 +1,23 @@ +, 'mod': int, 'val': int} */ class SignedUrl implements Plugin { @@ -37,26 +44,35 @@ class SignedUrl implements Plugin private const URL_QUERY_TOKEN_KEY = '_debug'; private const ISSUER_ID = 'cz.redbit.debug.url'; - /** @var resource|string */ - private $key; - private string $algorithm; private ?string $audience; private ?int $timestamp; + private JWTImpl $jwt; + /** - * @param string|resource $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 */ - public function __construct($key, string $algorithm = 'HS256', ?string $audience = null) + public function __construct(JWTImpl $jwt, ?string $audience = null) { - if (class_exists(JWT::class) === false) { - throw new LogicException(__CLASS__ . ' requires JWT library: firebase/php-jwt'); + $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' + * @noinspection PhpRedundantVariableDocTypeInspection + */ + 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()) { + return new self(new $impl($key, $algorithm), $audience); + } } - $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'); } /** @@ -115,7 +131,7 @@ public function getToken( 'val' => $value, ]; - return JWT::encode($payload, $this->key, $this->algorithm); + return $this->jwt->encode($payload); } public function __invoke(Detector $detector): ?bool @@ -153,7 +169,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'); @@ -226,8 +242,7 @@ public function verifyUrl(string $url, bool $allowRedirect = false): array public function verifyToken(string $token): array { try { - /** @var ClaimsSet $payload */ - $payload = JWT::decode($token, $this->key, [$this->algorithm]); + $payload = $this->jwt->decode($token); } catch (RuntimeException $e) { throw new SignedUrlVerificationException('JWT Token invalid', 0, $e); } @@ -324,6 +339,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); @@ -342,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; } } diff --git a/src/Plugin/SignedUrlVerificationException.php b/src/Plugin/SignedUrlVerificationException.php index 5b6e61a..9520751 100644 --- a/src/Plugin/SignedUrlVerificationException.php +++ b/src/Plugin/SignedUrlVerificationException.php @@ -1,7 +1,7 @@ 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 9fbf941..c324b60 100644 --- a/tests/Plugin/SignUrlTest.php +++ b/tests/Plugin/SignUrlTest.php @@ -1,7 +1,11 @@ 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); } @@ -36,10 +54,22 @@ 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); - $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); } @@ -47,10 +77,22 @@ 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); - $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); } @@ -58,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', @@ -67,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); } @@ -76,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', @@ -86,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); @@ -100,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); @@ -118,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); @@ -136,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'); @@ -148,20 +202,20 @@ public function testVerifyRequest(): void Assert::equal($expected, $parsed); } - public function testSignInvalidUrl() + public function testSignInvalidUrl(): void { - Assert::exception(function () { + 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); } - 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 = SignedUrl::create(self::KEY_HS256, 'HS256'); $plugin->signUrl($url, 1600000600); }, LogicException::class); } @@ -172,31 +226,31 @@ 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(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 () { - $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); + Assert::exception(static function () { + $plugin = SignedUrl::create(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 () { - $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); + Assert::exception(static function () { + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256'); $plugin->verifyUrl('https://host.tld/path?query=value'); }, SignedUrlVerificationException::class, 'No token in URL'); } @@ -206,15 +260,15 @@ 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); $tokenUrl .= '&fbclid=123456789'; Assert::exception( - function () use ($timestamp, $tokenUrl) { - $plugin = new SignedUrl(self::KEY_HS256, 'HS256'); + static function () use ($timestamp, $tokenUrl) { + $plugin = SignedUrl::create(self::KEY_HS256, 'HS256'); $plugin->setTimestamp($timestamp); JWT::$timestamp = $timestamp; $plugin->verifyUrl($tokenUrl); @@ -227,14 +281,20 @@ 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 { + $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.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); } }; @@ -248,16 +308,21 @@ 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'; // 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.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c' + . '&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGV' + . 'zdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGR' + . 'cL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjt' + . 'VFJep_t_qoQ5c' . '#hash'; Assert::equal($expected, $canonicalUrl); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 5183e09..c75a09b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,7 +1,7 @@