Skip to content

Commit 510a00c

Browse files
authored
fix: readme examples, add tests for all examples (#626)
1 parent 28aa069 commit 510a00c

File tree

3 files changed

+238
-36
lines changed

3 files changed

+238
-36
lines changed

README.md

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@ php env does not have libsodium installed:
2323
composer require paragonie/sodium_compat
2424
```
2525

26-
Example
27-
-------
26+
## Example
27+
2828
```php
2929
use Firebase\JWT\JWT;
3030
use Firebase\JWT\Key;
3131

32-
$key = 'example_key';
32+
$key = 'example_key_of_sufficient_length';
3333
$payload = [
34-
'iss' => 'http://example.org',
35-
'aud' => 'http://example.com',
34+
'iss' => 'example.org',
35+
'aud' => 'example.com',
3636
'iat' => 1356999524,
3737
'nbf' => 1357000000
3838
];
@@ -69,8 +69,9 @@ $decoded_array = (array) $decoded;
6969
JWT::$leeway = 60; // $leeway in seconds
7070
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
7171
```
72-
Example encode/decode headers
73-
-------
72+
73+
## Example encode/decode headers
74+
7475
Decoding the JWT headers without verifying the JWT first is NOT recommended, and is not supported by
7576
this library. This is because without verifying the JWT, the header values could have been tampered with.
7677
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:
8081
```php
8182
use Firebase\JWT\JWT;
8283

83-
$key = 'example_key';
84+
$key = 'example_key_of_sufficient_length';
8485
$payload = [
85-
'iss' => 'http://example.org',
86-
'aud' => 'http://example.com',
86+
'iss' => 'example.org',
87+
'aud' => 'example.com',
8788
'iat' => 1356999524,
8889
'nbf' => 1357000000
8990
];
@@ -103,8 +104,9 @@ $decoded = json_decode(base64_decode($headersB64), true);
103104

104105
print_r($decoded);
105106
```
106-
Example with RS256 (openssl)
107-
----------------------------
107+
108+
## Example with RS256 (openssl)
109+
108110
```php
109111
use Firebase\JWT\JWT;
110112
use Firebase\JWT\Key;
@@ -172,8 +174,7 @@ $decoded_array = (array) $decoded;
172174
echo "Decode:\n" . print_r($decoded_array, true) . "\n";
173175
```
174176

175-
Example with a passphrase
176-
-------------------------
177+
## Example with a passphrase
177178

178179
```php
179180
use Firebase\JWT\JWT;
@@ -209,8 +210,8 @@ $decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
209210
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
210211
```
211212

212-
Example with EdDSA (libsodium and Ed25519 signature)
213-
----------------------------
213+
## Example with EdDSA (libsodium and Ed25519 signature)
214+
214215
```php
215216
use Firebase\JWT\JWT;
216217
use Firebase\JWT\Key;
@@ -238,21 +239,21 @@ echo "Encode:\n" . print_r($jwt, true) . "\n";
238239

239240
$decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA'));
240241
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
241-
````
242+
```
243+
244+
## Example with multiple keys
242245

243-
Example with multiple keys
244-
--------------------------
245246
```php
246247
use Firebase\JWT\JWT;
247248
use Firebase\JWT\Key;
248249

249250
// Example RSA keys from previous example
250-
// $privateKey1 = '...';
251-
// $publicKey1 = '...';
251+
// $privateRsKey = '...';
252+
// $publicRsKey = '...';
252253

253254
// Example EdDSA keys from previous example
254-
// $privateKey2 = '...';
255-
// $publicKey2 = '...';
255+
// $privateEcKey = '...';
256+
// $publicEcKey = '...';
256257

257258
$payload = [
258259
'iss' => 'example.org',
@@ -261,14 +262,14 @@ $payload = [
261262
'nbf' => 1357000000
262263
];
263264

264-
$jwt1 = JWT::encode($payload, $privateKey1, 'RS256', 'kid1');
265-
$jwt2 = JWT::encode($payload, $privateKey2, 'EdDSA', 'kid2');
265+
$jwt1 = JWT::encode($payload, $privateRsKey, 'RS256', 'kid1');
266+
$jwt2 = JWT::encode($payload, $privateEcKey, 'EdDSA', 'kid2');
266267
echo "Encode 1:\n" . print_r($jwt1, true) . "\n";
267268
echo "Encode 2:\n" . print_r($jwt2, true) . "\n";
268269

269270
$keys = [
270-
'kid1' => new Key($publicKey1, 'RS256'),
271-
'kid2' => new Key($publicKey2, 'EdDSA'),
271+
'kid1' => new Key($publicRsKey, 'RS256'),
272+
'kid2' => new Key($publicEcKey, 'EdDSA'),
272273
];
273274

274275
$decoded1 = JWT::decode($jwt1, $keys);
@@ -278,8 +279,7 @@ echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n";
278279
echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n";
279280
```
280281

281-
Using JWKs
282-
----------
282+
## Using JWKs
283283

284284
```php
285285
use Firebase\JWT\JWK;
@@ -291,11 +291,11 @@ $jwks = ['keys' => []];
291291

292292
// JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key
293293
// objects. Pass this as the second parameter to JWT::decode.
294-
JWT::decode($jwt, JWK::parseKeySet($jwks));
294+
$decoded = JWT::decode($jwt, JWK::parseKeySet($jwks));
295+
print_r($decoded);
295296
```
296297

297-
Using Cached Key Sets
298-
---------------------
298+
## Using Cached Key Sets
299299

300300
The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI.
301301
This has the following advantages:
@@ -315,7 +315,7 @@ $jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk';
315315
$httpClient = new GuzzleHttp\Client();
316316

317317
// Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory)
318-
$httpFactory = new GuzzleHttp\Psr\HttpFactory();
318+
$httpFactory = new GuzzleHttp\Psr7\HttpFactory();
319319

320320
// Create a cache item pool (can be any PSR-6 compatible cache item pool)
321321
$cacheItemPool = Phpfastcache\CacheManager::getInstance('files');
@@ -406,8 +406,8 @@ Tests
406406
Run the tests using phpunit:
407407

408408
```bash
409-
$ pear install PHPUnit
410-
$ phpunit --configuration phpunit.xml.dist
409+
$ composer update
410+
$ vendor/bin/phpunit -c phpunit.xml.dist
411411
PHPUnit 3.7.10 by Sebastian Bergmann.
412412
.....
413413
Time: 0 seconds, Memory: 2.50Mb

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"phpunit/phpunit": "^9.5",
3838
"psr/cache": "^2.0||^3.0",
3939
"psr/http-client": "^1.0",
40-
"psr/http-factory": "^1.0"
40+
"psr/http-factory": "^1.0",
41+
"phpfastcache/phpfastcache": "^9.2"
4142
}
4243
}

tests/ReadmeTest.php

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
<?php
2+
3+
namespace Firebase\JWT;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
class ReadmeTest extends TestCase
8+
{
9+
private const CODEBLOCK_REGEX = '/^(?m)(\s*)(`{3,}|~{3,})[ \t]*(.*?)\n([\s\S]*?)\1\2\s*$/m';
10+
11+
private array $payload = [
12+
'iss' => 'example.org',
13+
'aud' => 'example.com',
14+
'iat' => 1356999524,
15+
'nbf' => 1357000000,
16+
];
17+
18+
public function testExample()
19+
{
20+
$codeblock = $this->extractCodeBlock('Example');
21+
$output = $codeblock->invoke();
22+
23+
$header = ['typ' => 'JWT', 'alg' => 'HS256'];
24+
25+
$this->assertEquals(
26+
print_r((object) $this->payload, true) . print_r((object) $header, true),
27+
$output
28+
);
29+
}
30+
31+
public function testExampleEncodeDecodeHeaders()
32+
{
33+
$codeblock = $this->extractCodeBlock('Example encode/decode headers');
34+
$output = $codeblock->invoke();
35+
36+
$header = [
37+
'typ' => 'JWT',
38+
'x-forwarded-for' => 'www.google.com',
39+
'alg' => 'HS256',
40+
];
41+
42+
$this->assertEquals(
43+
print_r($header, true),
44+
$output
45+
);
46+
}
47+
48+
public function testExampleWithRS256()
49+
{
50+
$codeblock = $this->extractCodeBlock('Example with RS256 (openssl)');
51+
$output = $codeblock->invoke();
52+
53+
$this->assertStringContainsString(
54+
"Decode:\n" . print_r($this->payload, true),
55+
$output
56+
);
57+
}
58+
59+
public function testExampleWithPassphrase()
60+
{
61+
$codeblock = $this->extractCodeBlock('Example with a passphrase');
62+
63+
$codeblock->replace('[YOUR_PASSPHRASE]', 'passphrase');
64+
$codeblock->replace(
65+
'/path/to/key-with-passphrase.pem',
66+
__DIR__ . '/data/rsa-with-passphrase.pem'
67+
);
68+
69+
$output = $codeblock->invoke();
70+
71+
$this->assertStringContainsString(
72+
"Decode:\n" . print_r($this->payload, true),
73+
$output
74+
);
75+
}
76+
77+
public function testExampleWithEdDSA()
78+
{
79+
$codeblock = $this->extractCodeBlock('Example with EdDSA (libsodium and Ed25519 signature)');
80+
81+
$output = $codeblock->invoke();
82+
83+
$this->assertStringContainsString(
84+
"Decode:\n" . print_r($this->payload, true),
85+
$output
86+
);
87+
}
88+
89+
public function testExampleWithMultipleKeys()
90+
{
91+
$codeblock = $this->extractCodeBlock('Example with multiple keys');
92+
93+
$keys = [
94+
'$privateRsKey' => 'rsa1-private.pem',
95+
'$publicRsKey' => 'rsa1-public.pub',
96+
'$privateEcKey' => 'ed25519-1.sec',
97+
'$publicEcKey' => 'ed25519-1.pub',
98+
];
99+
foreach ($keys as $varName => $keyFile) {
100+
$codeblock->replace(
101+
\sprintf('// %s = \'...\'', $varName),
102+
\sprintf('%s = file_get_contents(\'%s/data/%s\')', $varName, __DIR__, $keyFile)
103+
);
104+
}
105+
106+
$output = $codeblock->invoke();
107+
108+
$this->assertStringContainsString(
109+
"Decode 1:\n" . print_r($this->payload, true),
110+
$output
111+
);
112+
113+
$this->assertStringContainsString(
114+
"Decode 2:\n" . print_r($this->payload, true),
115+
$output
116+
);
117+
}
118+
119+
public function testUsingJWKs()
120+
{
121+
$codeblock = $this->extractCodeBlock('Using JWKs');
122+
123+
$privateKey = file_get_contents(__DIR__ . '/data/rsa1-private.pem');
124+
$jwt = JWT::encode($this->payload, $privateKey, 'RS256', 'jwk1');
125+
126+
$keysJson = file_get_contents(__DIR__ . '/data/rsa-jwkset.json');
127+
$jwkSet = json_decode($keysJson, true);
128+
129+
$codeblock->replace('$jwt', \sprintf("'%s'", $jwt));
130+
$codeblock->replace(
131+
'[\'keys\' => []]',
132+
var_export($jwkSet, true)
133+
);
134+
135+
$output = $codeblock->invoke();
136+
137+
$this->assertEquals(
138+
print_r((object) $this->payload, true),
139+
$output
140+
);
141+
}
142+
143+
public function testUsingCachedKeySets()
144+
{
145+
// We must accept a failure because we are not signing the keys
146+
// This is the farthest we can go without retreiving an actual JWT
147+
// or hosting our own JWKs url.
148+
$this->expectException(SignatureInvalidException::class);
149+
$this->expectExceptionMessage('Signature verification failed');
150+
151+
$codeblock = $this->extractCodeBlock('Using Cached Key Sets');
152+
153+
$privateKey = file_get_contents(__DIR__ . '/data/ecdsa256-private.pem');
154+
$jwt = JWT::encode($this->payload, $privateKey, 'ES256', '_xiGEQ');
155+
156+
$codeblock->replace('eyJhbGci...', $jwt);
157+
$codeblock->invoke();
158+
}
159+
160+
private function extractCodeBlock(string $header)
161+
{
162+
// Normalize line endings to \n to make regex handling consistent across platforms
163+
$markdown = str_replace(["\r\n", "\r"], "\n", file_get_contents(__DIR__ . '/../README.md'));
164+
165+
// find by header
166+
$pattern = '/^#+\s*' . preg_quote($header, '/') . '\s*\n([\s\S]*?)(?=^#+.*$|\Z)/m';
167+
if (!preg_match($pattern, $markdown, $matches)) {
168+
throw new \Exception('Header "' . $header . '" not found in README.md');
169+
}
170+
$markdown = trim($matches[1]);
171+
172+
// extract fenced codeblock
173+
if (!preg_match_all(self::CODEBLOCK_REGEX, $markdown, $matches, PREG_SET_ORDER)) {
174+
throw new \Exception('No code block found in README.md under header "' . $header . '"');
175+
}
176+
$codeblock = $matches[0][4];
177+
178+
return new class($codeblock) {
179+
public function __construct(public string $codeblock)
180+
{
181+
}
182+
183+
public function invoke()
184+
{
185+
try {
186+
ob_start();
187+
eval($this->codeblock);
188+
return ob_get_clean();
189+
} catch (\Exception $e) {
190+
ob_end_clean();
191+
throw $e;
192+
}
193+
}
194+
195+
public function replace($old, $new)
196+
{
197+
$this->codeblock = str_replace($old, $new, $this->codeblock);
198+
}
199+
};
200+
}
201+
}

0 commit comments

Comments
 (0)