Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 35 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
];
Expand Down Expand Up @@ -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
Expand All @@ -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
];
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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:
Expand All @@ -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');
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
201 changes: 201 additions & 0 deletions tests/ReadmeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php

namespace Firebase\JWT;

use PHPUnit\Framework\TestCase;

class ReadmeTest extends TestCase
{
private const CODEBLOCK_REGEX = '/^(?m)(\s*)(`{3,}|~{3,})[ \t]*(.*?)\n([\s\S]*?)\1\2\s*$/m';

private array $payload = [
'iss' => '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);
}
};
}
}
Loading