diff --git a/README.md b/README.md index 65b6c860..2ca71622 100644 --- a/README.md +++ b/README.md @@ -23,16 +23,16 @@ php env does not have libsodium installed: composer require paragonie/sodium_compat ``` -Example -------- +## Example + ```php use Firebase\JWT\JWT; use Firebase\JWT\Key; -$key = 'example_key'; +$key = 'example_key_of_sufficient_length'; $payload = [ - 'iss' => 'http://example.org', - 'aud' => 'http://example.com', + 'iss' => 'example.org', + 'aud' => 'example.com', 'iat' => 1356999524, 'nbf' => 1357000000 ]; @@ -69,8 +69,9 @@ $decoded_array = (array) $decoded; JWT::$leeway = 60; // $leeway in seconds $decoded = JWT::decode($jwt, new Key($key, 'HS256')); ``` -Example encode/decode headers -------- + +## Example encode/decode headers + Decoding the JWT headers without verifying the JWT first is NOT recommended, and is not supported by this library. This is because without verifying the JWT, the header values could have been tampered with. Any value pulled from an unverified header should be treated as if it could be any string sent in from an @@ -80,10 +81,10 @@ header part: ```php use Firebase\JWT\JWT; -$key = 'example_key'; +$key = 'example_key_of_sufficient_length'; $payload = [ - 'iss' => 'http://example.org', - 'aud' => 'http://example.com', + 'iss' => 'example.org', + 'aud' => 'example.com', 'iat' => 1356999524, 'nbf' => 1357000000 ]; @@ -103,8 +104,9 @@ $decoded = json_decode(base64_decode($headersB64), true); print_r($decoded); ``` -Example with RS256 (openssl) ----------------------------- + +## Example with RS256 (openssl) + ```php use Firebase\JWT\JWT; use Firebase\JWT\Key; @@ -172,8 +174,7 @@ $decoded_array = (array) $decoded; echo "Decode:\n" . print_r($decoded_array, true) . "\n"; ``` -Example with a passphrase -------------------------- +## Example with a passphrase ```php use Firebase\JWT\JWT; @@ -209,8 +210,8 @@ $decoded = JWT::decode($jwt, new Key($publicKey, 'RS256')); echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; ``` -Example with EdDSA (libsodium and Ed25519 signature) ----------------------------- +## Example with EdDSA (libsodium and Ed25519 signature) + ```php use Firebase\JWT\JWT; use Firebase\JWT\Key; @@ -238,21 +239,21 @@ echo "Encode:\n" . print_r($jwt, true) . "\n"; $decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA')); echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; -```` +``` + +## Example with multiple keys -Example with multiple keys --------------------------- ```php use Firebase\JWT\JWT; use Firebase\JWT\Key; // Example RSA keys from previous example -// $privateKey1 = '...'; -// $publicKey1 = '...'; +// $privateRsKey = '...'; +// $publicRsKey = '...'; // Example EdDSA keys from previous example -// $privateKey2 = '...'; -// $publicKey2 = '...'; +// $privateEcKey = '...'; +// $publicEcKey = '...'; $payload = [ 'iss' => 'example.org', @@ -261,14 +262,14 @@ $payload = [ 'nbf' => 1357000000 ]; -$jwt1 = JWT::encode($payload, $privateKey1, 'RS256', 'kid1'); -$jwt2 = JWT::encode($payload, $privateKey2, 'EdDSA', 'kid2'); +$jwt1 = JWT::encode($payload, $privateRsKey, 'RS256', 'kid1'); +$jwt2 = JWT::encode($payload, $privateEcKey, 'EdDSA', 'kid2'); echo "Encode 1:\n" . print_r($jwt1, true) . "\n"; echo "Encode 2:\n" . print_r($jwt2, true) . "\n"; $keys = [ - 'kid1' => new Key($publicKey1, 'RS256'), - 'kid2' => new Key($publicKey2, 'EdDSA'), + 'kid1' => new Key($publicRsKey, 'RS256'), + 'kid2' => new Key($publicEcKey, 'EdDSA'), ]; $decoded1 = JWT::decode($jwt1, $keys); @@ -278,8 +279,7 @@ echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n"; echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n"; ``` -Using JWKs ----------- +## Using JWKs ```php use Firebase\JWT\JWK; @@ -291,11 +291,11 @@ $jwks = ['keys' => []]; // JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key // objects. Pass this as the second parameter to JWT::decode. -JWT::decode($jwt, JWK::parseKeySet($jwks)); +$decoded = JWT::decode($jwt, JWK::parseKeySet($jwks)); +print_r($decoded); ``` -Using Cached Key Sets ---------------------- +## Using Cached Key Sets The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI. This has the following advantages: @@ -315,7 +315,7 @@ $jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; $httpClient = new GuzzleHttp\Client(); // Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory) -$httpFactory = new GuzzleHttp\Psr\HttpFactory(); +$httpFactory = new GuzzleHttp\Psr7\HttpFactory(); // Create a cache item pool (can be any PSR-6 compatible cache item pool) $cacheItemPool = Phpfastcache\CacheManager::getInstance('files'); @@ -406,8 +406,8 @@ Tests Run the tests using phpunit: ```bash -$ pear install PHPUnit -$ phpunit --configuration phpunit.xml.dist +$ composer update +$ vendor/bin/phpunit -c phpunit.xml.dist PHPUnit 3.7.10 by Sebastian Bergmann. ..... Time: 0 seconds, Memory: 2.50Mb diff --git a/composer.json b/composer.json index 816cfd0b..4b988631 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "phpunit/phpunit": "^9.5", "psr/cache": "^2.0||^3.0", "psr/http-client": "^1.0", - "psr/http-factory": "^1.0" + "psr/http-factory": "^1.0", + "phpfastcache/phpfastcache": "^9.2" } } diff --git a/tests/ReadmeTest.php b/tests/ReadmeTest.php new file mode 100644 index 00000000..aaa47858 --- /dev/null +++ b/tests/ReadmeTest.php @@ -0,0 +1,201 @@ + 'example.org', + 'aud' => 'example.com', + 'iat' => 1356999524, + 'nbf' => 1357000000, + ]; + + public function testExample() + { + $codeblock = $this->extractCodeBlock('Example'); + $output = $codeblock->invoke(); + + $header = ['typ' => 'JWT', 'alg' => 'HS256']; + + $this->assertEquals( + print_r((object) $this->payload, true) . print_r((object) $header, true), + $output + ); + } + + public function testExampleEncodeDecodeHeaders() + { + $codeblock = $this->extractCodeBlock('Example encode/decode headers'); + $output = $codeblock->invoke(); + + $header = [ + 'typ' => 'JWT', + 'x-forwarded-for' => 'www.google.com', + 'alg' => 'HS256', + ]; + + $this->assertEquals( + print_r($header, true), + $output + ); + } + + public function testExampleWithRS256() + { + $codeblock = $this->extractCodeBlock('Example with RS256 (openssl)'); + $output = $codeblock->invoke(); + + $this->assertStringContainsString( + "Decode:\n" . print_r($this->payload, true), + $output + ); + } + + public function testExampleWithPassphrase() + { + $codeblock = $this->extractCodeBlock('Example with a passphrase'); + + $codeblock->replace('[YOUR_PASSPHRASE]', 'passphrase'); + $codeblock->replace( + '/path/to/key-with-passphrase.pem', + __DIR__ . '/data/rsa-with-passphrase.pem' + ); + + $output = $codeblock->invoke(); + + $this->assertStringContainsString( + "Decode:\n" . print_r($this->payload, true), + $output + ); + } + + public function testExampleWithEdDSA() + { + $codeblock = $this->extractCodeBlock('Example with EdDSA (libsodium and Ed25519 signature)'); + + $output = $codeblock->invoke(); + + $this->assertStringContainsString( + "Decode:\n" . print_r($this->payload, true), + $output + ); + } + + public function testExampleWithMultipleKeys() + { + $codeblock = $this->extractCodeBlock('Example with multiple keys'); + + $keys = [ + '$privateRsKey' => 'rsa1-private.pem', + '$publicRsKey' => 'rsa1-public.pub', + '$privateEcKey' => 'ed25519-1.sec', + '$publicEcKey' => 'ed25519-1.pub', + ]; + foreach ($keys as $varName => $keyFile) { + $codeblock->replace( + \sprintf('// %s = \'...\'', $varName), + \sprintf('%s = file_get_contents(\'%s/data/%s\')', $varName, __DIR__, $keyFile) + ); + } + + $output = $codeblock->invoke(); + + $this->assertStringContainsString( + "Decode 1:\n" . print_r($this->payload, true), + $output + ); + + $this->assertStringContainsString( + "Decode 2:\n" . print_r($this->payload, true), + $output + ); + } + + public function testUsingJWKs() + { + $codeblock = $this->extractCodeBlock('Using JWKs'); + + $privateKey = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); + $jwt = JWT::encode($this->payload, $privateKey, 'RS256', 'jwk1'); + + $keysJson = file_get_contents(__DIR__ . '/data/rsa-jwkset.json'); + $jwkSet = json_decode($keysJson, true); + + $codeblock->replace('$jwt', \sprintf("'%s'", $jwt)); + $codeblock->replace( + '[\'keys\' => []]', + var_export($jwkSet, true) + ); + + $output = $codeblock->invoke(); + + $this->assertEquals( + print_r((object) $this->payload, true), + $output + ); + } + + public function testUsingCachedKeySets() + { + // We must accept a failure because we are not signing the keys + // This is the farthest we can go without retreiving an actual JWT + // or hosting our own JWKs url. + $this->expectException(SignatureInvalidException::class); + $this->expectExceptionMessage('Signature verification failed'); + + $codeblock = $this->extractCodeBlock('Using Cached Key Sets'); + + $privateKey = file_get_contents(__DIR__ . '/data/ecdsa256-private.pem'); + $jwt = JWT::encode($this->payload, $privateKey, 'ES256', '_xiGEQ'); + + $codeblock->replace('eyJhbGci...', $jwt); + $codeblock->invoke(); + } + + private function extractCodeBlock(string $header) + { + // Normalize line endings to \n to make regex handling consistent across platforms + $markdown = str_replace(["\r\n", "\r"], "\n", file_get_contents(__DIR__ . '/../README.md')); + + // find by header + $pattern = '/^#+\s*' . preg_quote($header, '/') . '\s*\n([\s\S]*?)(?=^#+.*$|\Z)/m'; + if (!preg_match($pattern, $markdown, $matches)) { + throw new \Exception('Header "' . $header . '" not found in README.md'); + } + $markdown = trim($matches[1]); + + // extract fenced codeblock + if (!preg_match_all(self::CODEBLOCK_REGEX, $markdown, $matches, PREG_SET_ORDER)) { + throw new \Exception('No code block found in README.md under header "' . $header . '"'); + } + $codeblock = $matches[0][4]; + + return new class($codeblock) { + public function __construct(public string $codeblock) + { + } + + public function invoke() + { + try { + ob_start(); + eval($this->codeblock); + return ob_get_clean(); + } catch (\Exception $e) { + ob_end_clean(); + throw $e; + } + } + + public function replace($old, $new) + { + $this->codeblock = str_replace($old, $new, $this->codeblock); + } + }; + } +}