update packages

This commit is contained in:
2026-05-28 18:44:36 +03:30
parent 9dbdfa7e21
commit 9b7405e64a
45 changed files with 2531 additions and 136 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
{
"name": "dabestaniha/authenticate-bridge",
"require": {
"php": "^8.2",
"firebase/php-jwt": "^6.11"
"php": "^8.1",
"firebase/php-jwt": "^7.0"
},
"autoload": {
"psr-4": {
Generated
+11 -10
View File
@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "775143d1bcec2aacbb576adc03b110ae",
"content-hash": "20c98790ea3a0bf0f64fa300f73ec329",
"packages": [
{
"name": "firebase/php-jwt",
"version": "v6.11.1",
"version": "v7.0.5",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
"url": "https://github.com/googleapis/php-jwt.git",
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380",
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380",
"shasum": ""
},
"require": {
@@ -25,6 +25,7 @@
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpfastcache/phpfastcache": "^9.2",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
@@ -64,10 +65,10 @@
"php"
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
"issues": "https://github.com/googleapis/php-jwt/issues",
"source": "https://github.com/googleapis/php-jwt/tree/v7.0.5"
},
"time": "2025-04-09T20:32:01+00:00"
"time": "2026-04-01T20:38:03+00:00"
}
],
"packages-dev": [],
@@ -77,7 +78,7 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^8.2"
"php": "^8.1"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
+4 -1
View File
@@ -14,7 +14,10 @@ if (PHP_VERSION_ID < 50600) {
echo $err;
}
}
throw new RuntimeException($err);
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
+1 -19
View File
@@ -26,12 +26,6 @@ use Composer\Semver\VersionParser;
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
@@ -328,18 +322,6 @@ class InstalledVersions
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
@@ -354,7 +336,7 @@ class InstalledVersions
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
$selfDir = strtr(__DIR__, '\\', '/');
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
+11 -10
View File
@@ -2,17 +2,17 @@
"packages": [
{
"name": "firebase/php-jwt",
"version": "v6.11.1",
"version_normalized": "6.11.1.0",
"version": "v7.0.5",
"version_normalized": "7.0.5.0",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
"url": "https://github.com/googleapis/php-jwt.git",
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380",
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380",
"shasum": ""
},
"require": {
@@ -20,6 +20,7 @@
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpfastcache/phpfastcache": "^9.2",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
@@ -30,9 +31,9 @@
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"time": "2025-04-09T20:32:01+00:00",
"time": "2026-04-01T20:38:03+00:00",
"type": "library",
"installation-source": "dist",
"installation-source": "source",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
@@ -61,8 +62,8 @@
"php"
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
"issues": "https://github.com/googleapis/php-jwt/issues",
"source": "https://github.com/googleapis/php-jwt/tree/v7.0.5"
},
"install-path": "../firebase/php-jwt"
}
+5 -5
View File
@@ -3,7 +3,7 @@
'name' => 'dabestaniha/authenticate-bridge',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '89a7eed84730a7731c7c74cfba53878925b4ccbe',
'reference' => '9dbdfa7e21bb4c0fca40b1f84265c0ad559c133c',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -13,16 +13,16 @@
'dabestaniha/authenticate-bridge' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '89a7eed84730a7731c7c74cfba53878925b4ccbe',
'reference' => '9dbdfa7e21bb4c0fca40b1f84265c0ad559c133c',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'firebase/php-jwt' => array(
'pretty_version' => 'v6.11.1',
'version' => '6.11.1.0',
'reference' => 'd1e91ecf8c598d073d0995afa8cd5c75c6e19e66',
'pretty_version' => 'v7.0.5',
'version' => '7.0.5.0',
'reference' => '47ad26bab5e7c70ae8a6f08ed25ff83631121380',
'type' => 'library',
'install_path' => __DIR__ . '/../firebase/php-jwt',
'aliases' => array(),
+5 -4
View File
@@ -4,8 +4,8 @@
$issues = array();
if (!(PHP_VERSION_ID >= 80200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
@@ -19,7 +19,8 @@ if ($issues) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}
+9
View File
@@ -0,0 +1,9 @@
* text=auto
/.gitattributes export-ignore
/.gitignore export-ignore
/.github export-ignore
/.php-cs-fixer.dist.php export-ignore
/phpstan.neon.dist export-ignore
/phpunit.xml.dist export-ignore
/tests export-ignore
@@ -0,0 +1,20 @@
name: release-please
on:
push:
branches:
- main
permissions:
# Needed for Release Please to create and update files
contents: write
# Needed for Release Please to create Release PRs
pull-requests: write
jobs:
release-please:
environment: Release
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.YOSHI_CODE_BOT_TOKEN }}
release-type: simple
+57
View File
@@ -0,0 +1,57 @@
name: Test Suite
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php: [ "8.0", "8.1", "8.2", "8.3", "8.4", "8.5" ]
name: PHP ${{matrix.php }} Unit Test
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Install Dependencies
uses: nick-invision/retry@v1
with:
timeout_minutes: 10
max_attempts: 3
command: composer install
- name: Run Script
run: vendor/bin/phpunit
style:
runs-on: ubuntu-latest
name: PHP Style Check
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
- name: Run Script
run: |
composer global require friendsofphp/php-cs-fixer
~/.composer/vendor/bin/php-cs-fixer fix --diff --dry-run --allow-risky=yes .
staticanalysis:
runs-on: ubuntu-latest
name: PHPStan Static Analysis
steps:
- uses: actions/checkout@v2
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Run Script
run: |
composer install
composer global require phpstan/phpstan:~1.10.0
~/.composer/vendor/bin/phpstan analyse
+7
View File
@@ -0,0 +1,7 @@
vendor
phpunit.phar
phpunit.phar.asc
composer.phar
composer.lock
.phpunit.result.cache
.php-cs-fixer.cache
+28
View File
@@ -0,0 +1,28 @@
<?php
return (new PhpCsFixer\Config())
->setRules([
'@PSR2' => true,
'concat_space' => ['spacing' => 'one'],
'no_unused_imports' => true,
'ordered_imports' => true,
'new_with_braces' => true,
'method_argument_space' => false,
'whitespace_after_comma_in_array' => true,
'return_type_declaration' => [
'space_before' => 'none'
],
'single_quote' => true,
'native_function_invocation' => [
'strict' => false
],
'nullable_type_declaration' => [
'syntax' => 'question_mark',
],
'nullable_type_declaration_for_default_null_value' => true,
])
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__)
)
;
+46
View File
@@ -1,5 +1,51 @@
# Changelog
## [7.0.5](https://github.com/firebase/php-jwt/compare/v7.0.4...v7.0.5) (2026-03-31)
### Bug Fixes
* RSA from JWK sometimes returns empty Instance ([#628](https://github.com/firebase/php-jwt/issues/628)) ([b4c78aa](https://github.com/firebase/php-jwt/commit/b4c78aa731664122198ad36c0033aa29e807397a))
## [7.0.4](https://github.com/firebase/php-jwt/compare/v7.0.3...v7.0.4) (2026-03-27)
### Bug Fixes
* readme examples, add tests for all examples ([#626](https://github.com/firebase/php-jwt/issues/626)) ([510a00c](https://github.com/firebase/php-jwt/commit/510a00c0e6353bc7d68412fab67e57a13954cb46))
* use urlsafeB64Decode everywhere ([#627](https://github.com/firebase/php-jwt/issues/627)) ([b889495](https://github.com/firebase/php-jwt/commit/b889495c83ddc3f3885ca3f0b65b41b1cb37a3b1))
## [7.0.3](https://github.com/firebase/php-jwt/compare/v7.0.2...v7.0.3) (2026-02-18)
### Miscellaneous Chores
* add environment for Release Please job ([#619](https://github.com/firebase/php-jwt/issues/619)) ([300fd02](https://github.com/firebase/php-jwt/commit/300fd02c883f096c9067df652dbd23f62cb5e2a7))
## [7.0.2](https://github.com/firebase/php-jwt/compare/v7.0.1...v7.0.2) (2025-12-16)
### Bug Fixes
* add key length validation for ec keys ([#615](https://github.com/firebase/php-jwt/issues/615)) ([7044f9a](https://github.com/firebase/php-jwt/commit/7044f9ae7e7d175d28cca71714feb236f1c0e252))
## [7.0.0](https://github.com/firebase/php-jwt/compare/v6.11.1...v7.0.0) (2025-12-15)
### ⚠️ ⚠️ ⚠️ Security Fixes ⚠️ ⚠️ ⚠️
* add key size validation ([#613](https://github.com/firebase/php-jwt/issues/613)) ([6b80341](https://github.com/firebase/php-jwt/commit/6b80341bf57838ea2d011487917337901cd71576))
**NOTE**: This fix will cause keys with a size below the minimally allowed size to break.
### Features
* add SensitiveParameter attribute to security-critical parameters ([#603](https://github.com/firebase/php-jwt/issues/603)) ([4dbfac0](https://github.com/firebase/php-jwt/commit/4dbfac0260eeb0e9e643063c99998e3219cc539b))
* store timestamp in `ExpiredException` ([#604](https://github.com/firebase/php-jwt/issues/604)) ([f174826](https://github.com/firebase/php-jwt/commit/f1748260d218a856b6a0c23715ac7fae1d7ca95b))
### Bug Fixes
* validate iat and nbf on payload ([#568](https://github.com/firebase/php-jwt/issues/568)) ([953b2c8](https://github.com/firebase/php-jwt/commit/953b2c88bb445b7e3bb82a5141928f13d7343afd))
## [6.11.1](https://github.com/firebase/php-jwt/compare/v6.11.0...v6.11.1) (2025-04-09)
+36 -36
View File
@@ -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;
@@ -186,7 +187,7 @@ $passphrase = '[YOUR_PASSPHRASE]';
// Can be generated with "ssh-keygen -t rsa -m pem"
$privateKeyFile = '/path/to/key-with-passphrase.pem';
// Create a private key of type "resource"
/** @var OpenSSLAsymmetricKey $privateKey */
$privateKey = openssl_pkey_get_private(
file_get_contents($privateKeyFile),
$passphrase
@@ -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
+2 -1
View File
@@ -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"
}
}
+5
View File
@@ -0,0 +1,5 @@
parameters:
level: 7
paths:
- src
treatPhpDocTypesAsCertain: false
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="PHP JSON Web Token Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
</phpunit>
+2 -1
View File
@@ -180,7 +180,8 @@ class CachedKeySet implements ArrayAccess
$jwksResponse = $this->httpClient->sendRequest($request);
if ($jwksResponse->getStatusCode() !== 200) {
throw new UnexpectedValueException(
\sprintf('HTTP Error: %d %s for URI "%s"',
\sprintf(
'HTTP Error: %d %s for URI "%s"',
$jwksResponse->getStatusCode(),
$jwksResponse->getReasonPhrase(),
$this->jwksUri,
+12
View File
@@ -6,6 +6,8 @@ class ExpiredException extends \UnexpectedValueException implements JWTException
{
private object $payload;
private ?int $timestamp = null;
public function setPayload(object $payload): void
{
$this->payload = $payload;
@@ -15,4 +17,14 @@ class ExpiredException extends \UnexpectedValueException implements JWTException
{
return $this->payload;
}
public function setTimestamp(int $timestamp): void
{
$this->timestamp = $timestamp;
}
public function getTimestamp(): ?int
{
return $this->timestamp;
}
}
+10 -2
View File
@@ -52,7 +52,7 @@ class JWK
*
* @uses parseKey
*/
public static function parseKeySet(array $jwks, ?string $defaultAlg = null): array
public static function parseKeySet(#[\SensitiveParameter] array $jwks, ?string $defaultAlg = null): array
{
$keys = [];
@@ -93,7 +93,7 @@ class JWK
*
* @uses createPemFromModulusAndExponent
*/
public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key
public static function parseKey(#[\SensitiveParameter] array $jwk, ?string $defaultAlg = null): ?Key
{
if (empty($jwk)) {
throw new InvalidArgumentException('JWK must not be empty');
@@ -240,6 +240,14 @@ class JWK
): string {
$mod = JWT::urlsafeB64Decode($n);
$exp = JWT::urlsafeB64Decode($e);
// Correct encoding for ASN1, as ints are represented as unsigned in jwk
// but signed in ASN1. Prepending null byte makes it unsigned.
if (\strlen($mod) > 0 && \ord($mod[0]) >= 128) {
$mod = \chr(0) . $mod;
}
if (\strlen($exp) > 0 && \ord($exp[0]) >= 128) {
$exp = \chr(0) . $exp;
}
$modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
$publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
+118 -40
View File
@@ -31,6 +31,8 @@ class JWT
private const ASN1_SEQUENCE = 0x10;
private const ASN1_BIT_STRING = 0x03;
private const RSA_KEY_MIN_LENGTH = 2048;
/**
* When checking nbf, iat or expiration times,
* we want to provide some extra leeway time to
@@ -95,7 +97,7 @@ class JWT
*/
public static function decode(
string $jwt,
$keyOrKeyArray,
#[\SensitiveParameter] $keyOrKeyArray,
?stdClass &$headers = null
): stdClass {
// Validate JWT
@@ -127,6 +129,16 @@ class JWT
if (!$payload instanceof stdClass) {
throw new UnexpectedValueException('Payload must be a JSON object');
}
if (isset($payload->iat) && !\is_numeric($payload->iat)) {
throw new UnexpectedValueException('Payload iat must be a number');
}
if (isset($payload->nbf) && !\is_numeric($payload->nbf)) {
throw new UnexpectedValueException('Payload nbf must be a number');
}
if (isset($payload->exp) && !\is_numeric($payload->exp)) {
throw new UnexpectedValueException('Payload exp must be a number');
}
$sig = static::urlsafeB64Decode($cryptob64);
if (empty($header->alg)) {
throw new UnexpectedValueException('Empty algorithm');
@@ -154,7 +166,7 @@ class JWT
// token can actually be used. If it's not yet that time, abort.
if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) {
$ex = new BeforeValidException(
'Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) floor($payload->nbf))
'Cannot handle token with nbf prior to ' . \date(DateTime::ATOM, (int) floor($payload->nbf))
);
$ex->setPayload($payload);
throw $ex;
@@ -165,7 +177,7 @@ class JWT
// correctly used the nbf claim).
if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) {
$ex = new BeforeValidException(
'Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) floor($payload->iat))
'Cannot handle token with iat prior to ' . \date(DateTime::ATOM, (int) floor($payload->iat))
);
$ex->setPayload($payload);
throw $ex;
@@ -175,6 +187,7 @@ class JWT
if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
$ex = new ExpiredException('Expired token');
$ex->setPayload($payload);
$ex->setTimestamp($timestamp);
throw $ex;
}
@@ -185,11 +198,11 @@ class JWT
* Converts and signs a PHP array into a JWT string.
*
* @param array<mixed> $payload PHP array
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256',
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
* @param string $keyId
* @param array<string, string> $head An array with header elements to attach
* @param array<string, string|string[]> $head An array with header elements to attach
*
* @return string A signed JWT
*
@@ -198,7 +211,7 @@ class JWT
*/
public static function encode(
array $payload,
$key,
#[\SensitiveParameter] $key,
string $alg,
?string $keyId = null,
?array $head = null
@@ -226,7 +239,7 @@ class JWT
* Sign a string with a given key and algorithm.
*
* @param string $msg The message to sign
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256',
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
*
@@ -236,7 +249,7 @@ class JWT
*/
public static function sign(
string $msg,
$key,
#[\SensitiveParameter] $key,
string $alg
): string {
if (empty(static::$supported_algs[$alg])) {
@@ -248,13 +261,19 @@ class JWT
if (!\is_string($key)) {
throw new InvalidArgumentException('key must be a string when using hmac');
}
self::validateHmacKeyLength($key, $algorithm);
return \hash_hmac($algorithm, $msg, $key, true);
case 'openssl':
$signature = '';
if (!\is_resource($key) && !openssl_pkey_get_private($key)) {
if (!$key = openssl_pkey_get_private($key)) {
throw new DomainException('OpenSSL unable to validate key');
}
$success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line
if (str_starts_with($alg, 'RS')) {
self::validateRsaKeyLength($key);
} elseif (str_starts_with($alg, 'ES')) {
self::validateEcKeyLength($key, $alg);
}
$success = \openssl_sign($msg, $signature, $key, $algorithm);
if (!$success) {
throw new DomainException('OpenSSL unable to sign data');
}
@@ -265,20 +284,8 @@ class JWT
}
return $signature;
case 'sodium_crypto':
if (!\function_exists('sodium_crypto_sign_detached')) {
throw new DomainException('libsodium is not available');
}
if (!\is_string($key)) {
throw new InvalidArgumentException('key must be a string when using EdDSA');
}
try {
// The last non-empty line is used as the key.
$lines = array_filter(explode("\n", $key));
$key = base64_decode((string) end($lines));
if (\strlen($key) === 0) {
throw new DomainException('Key cannot be empty string');
}
return sodium_crypto_sign_detached($msg, $key);
return sodium_crypto_sign_detached($msg, self::validateEdDSAKey($key));
} catch (Exception $e) {
throw new DomainException($e->getMessage(), 0, $e);
}
@@ -293,7 +300,7 @@ class JWT
*
* @param string $msg The original message (header and body)
* @param string $signature The original signature
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
* @param string $alg The algorithm
*
* @return bool
@@ -303,7 +310,7 @@ class JWT
private static function verify(
string $msg,
string $signature,
$keyMaterial,
#[\SensitiveParameter] $keyMaterial,
string $alg
): bool {
if (empty(static::$supported_algs[$alg])) {
@@ -313,7 +320,15 @@ class JWT
list($function, $algorithm) = static::$supported_algs[$alg];
switch ($function) {
case 'openssl':
$success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line
if (!$key = openssl_pkey_get_public($keyMaterial)) {
throw new DomainException('OpenSSL unable to validate key');
}
if (str_starts_with($alg, 'RS')) {
self::validateRsaKeyLength($key);
} elseif (str_starts_with($alg, 'ES')) {
self::validateEcKeyLength($key, $alg);
}
$success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm);
if ($success === 1) {
return true;
}
@@ -325,19 +340,8 @@ class JWT
'OpenSSL error: ' . \openssl_error_string()
);
case 'sodium_crypto':
if (!\function_exists('sodium_crypto_sign_verify_detached')) {
throw new DomainException('libsodium is not available');
}
if (!\is_string($keyMaterial)) {
throw new InvalidArgumentException('key must be a string when using EdDSA');
}
try {
// The last non-empty line is used as the key.
$lines = array_filter(explode("\n", $keyMaterial));
$key = base64_decode((string) end($lines));
if (\strlen($key) === 0) {
throw new DomainException('Key cannot be empty string');
}
$key = self::validateEdDSAKey($keyMaterial);
if (\strlen($signature) === 0) {
throw new DomainException('Signature cannot be empty string');
}
@@ -350,6 +354,7 @@ class JWT
if (!\is_string($keyMaterial)) {
throw new InvalidArgumentException('key must be a string when using hmac');
}
self::validateHmacKeyLength($keyMaterial, $algorithm);
$hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
return self::constantTimeEquals($hash, $signature);
}
@@ -445,7 +450,6 @@ class JWT
return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
}
/**
* Determine if an algorithm has been provided for each Key
*
@@ -457,7 +461,7 @@ class JWT
* @return Key
*/
private static function getKey(
$keyOrKeyArray,
#[\SensitiveParameter] $keyOrKeyArray,
?string $kid
): Key {
if ($keyOrKeyArray instanceof Key) {
@@ -664,4 +668,78 @@ class JWT
return [$pos, $data];
}
/**
* Validate HMAC key length
*
* @param string $key HMAC key material
* @param string $algorithm The algorithm
*
* @throws DomainException Provided key is too short
*/
private static function validateHmacKeyLength(string $key, string $algorithm): void
{
$keyLength = \strlen($key) * 8;
$minKeyLength = (int) \str_replace('SHA', '', $algorithm);
if ($keyLength < $minKeyLength) {
throw new DomainException('Provided key is too short');
}
}
/**
* Validate RSA key length
*
* @param OpenSSLAsymmetricKey $key RSA key material
* @throws DomainException Provided key is too short
*/
private static function validateRsaKeyLength(#[\SensitiveParameter] OpenSSLAsymmetricKey $key): void
{
if (!$keyDetails = openssl_pkey_get_details($key)) {
throw new DomainException('Unable to validate key');
}
if ($keyDetails['bits'] < self::RSA_KEY_MIN_LENGTH) {
throw new DomainException('Provided key is too short');
}
}
/**
* Validate RSA key length
*
* @param OpenSSLAsymmetricKey $key RSA key material
* @param string $algorithm The algorithm
* @throws DomainException Provided key is too short
*/
private static function validateEcKeyLength(
#[\SensitiveParameter] OpenSSLAsymmetricKey $key,
string $algorithm
): void {
if (!$keyDetails = openssl_pkey_get_details($key)) {
throw new DomainException('Unable to validate key');
}
$minKeyLength = (int) \str_replace('ES', '', $algorithm);
if ($keyDetails['bits'] < $minKeyLength) {
throw new DomainException('Provided key is too short');
}
}
/**
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial
* @return non-empty-string
*/
private static function validateEdDSAKey(#[\SensitiveParameter] $keyMaterial): string
{
if (!\function_exists('sodium_crypto_sign_verify_detached')) {
throw new DomainException('libsodium is not available');
}
if (!\is_string($keyMaterial)) {
throw new InvalidArgumentException('key must be a string when using EdDSA');
}
// The last non-empty line is used as the key.
$lines = array_filter(explode("\n", $keyMaterial));
$key = self::urlsafeB64Decode((string) end($lines));
if (\strlen($key) === 0) {
throw new DomainException('Key cannot be empty string');
}
return $key;
}
}
+4 -5
View File
@@ -10,20 +10,19 @@ use TypeError;
class Key
{
/**
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial
* @param string $algorithm
*/
public function __construct(
private $keyMaterial,
#[\SensitiveParameter] private $keyMaterial,
private string $algorithm
) {
if (
!\is_string($keyMaterial)
&& !$keyMaterial instanceof OpenSSLAsymmetricKey
&& !$keyMaterial instanceof OpenSSLCertificate
&& !\is_resource($keyMaterial)
) {
throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey');
throw new TypeError('Key material must be a string, OpenSSLCertificate, or OpenSSLAsymmetricKey');
}
if (empty($keyMaterial)) {
@@ -46,7 +45,7 @@ class Key
}
/**
* @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate
* @return string|OpenSSLAsymmetricKey|OpenSSLCertificate
*/
public function getKeyMaterial()
{
+649
View File
@@ -0,0 +1,649 @@
<?php
namespace Firebase\JWT;
use LogicException;
use OutOfBoundsException;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use RuntimeException;
class CachedKeySetTest extends TestCase
{
use ProphecyTrait;
private $testJwksUri = 'https://jwk.uri';
private $testJwksUriKey = 'jwkshttpsjwk.uri';
private $testJwks1 = '{"keys": [{"kid":"foo","kty":"RSA","alg":"foo","n":"","e":""}]}';
private $testCachedJwks1 = ['foo' => ['kid' => 'foo', 'kty' => 'RSA', 'alg' => 'foo', 'n' => '', 'e' => '']];
private $testJwks2 = '{"keys": [{"kid":"bar","kty":"RSA","alg":"bar","n":"","e":""}]}';
private $testJwks3 = '{"keys": [{"kid":"baz","kty":"RSA","n":"","e":""}]}';
private $googleRsaUri = 'https://www.googleapis.com/oauth2/v3/certs';
private $googleEcUri = 'https://www.gstatic.com/iap/verify/public_key-jwk';
public function testEmptyUriThrowsException()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('JWKS URI is empty');
$cachedKeySet = new CachedKeySet(
'',
$this->prophesize(ClientInterface::class)->reveal(),
$this->prophesize(RequestFactoryInterface::class)->reveal(),
$this->prophesize(CacheItemPoolInterface::class)->reveal()
);
$cachedKeySet['foo'];
}
public function testOffsetSetThrowsException()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Method not implemented');
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->prophesize(ClientInterface::class)->reveal(),
$this->prophesize(RequestFactoryInterface::class)->reveal(),
$this->prophesize(CacheItemPoolInterface::class)->reveal()
);
$cachedKeySet['foo'] = 'bar';
}
public function testOffsetUnsetThrowsException()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Method not implemented');
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->prophesize(ClientInterface::class)->reveal(),
$this->prophesize(RequestFactoryInterface::class)->reveal(),
$this->prophesize(CacheItemPoolInterface::class)->reveal()
);
unset($cachedKeySet['foo']);
}
public function testOutOfBoundsThrowsException()
{
$this->expectException(OutOfBoundsException::class);
$this->expectExceptionMessage('Key ID not found');
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->getMockHttpClient($this->testJwks1),
$this->getMockHttpFactory(),
$this->getMockEmptyCache()
);
// keyID doesn't exist
$cachedKeySet['bar'];
}
public function testInvalidHttpResponseThrowsException()
{
$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessage('HTTP Error: 404 URL not found');
$this->expectExceptionCode(404);
$response = $this->prophesize('Psr\Http\Message\ResponseInterface');
$response->getStatusCode()
->shouldBeCalled()
->willReturn(404);
$response->getReasonPhrase()
->shouldBeCalledTimes(1)
->willReturn('URL not found');
$http = $this->prophesize(ClientInterface::class);
$http->sendRequest(Argument::any())
->shouldBeCalledTimes(1)
->willReturn($response->reveal());
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$http->reveal(),
$this->getMockHttpFactory(),
$this->getMockEmptyCache()
);
isset($cachedKeySet[0]);
}
public function testWithExistingKeyId()
{
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->getMockHttpClient($this->testJwks1),
$this->getMockHttpFactory(),
$this->getMockEmptyCache()
);
$this->assertInstanceOf(Key::class, $cachedKeySet['foo']);
$this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm());
}
public function testWithDefaultAlg()
{
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->getMockHttpClient($this->testJwks3),
$this->getMockHttpFactory(),
$this->getMockEmptyCache(),
null,
false,
'baz256'
);
$this->assertInstanceOf(Key::class, $cachedKeySet['baz']);
$this->assertSame('baz256', $cachedKeySet['baz']->getAlgorithm());
}
public function testKeyIdIsCached()
{
$cacheItem = $this->prophesize(CacheItemInterface::class);
$cacheItem->isHit()
->willReturn(true);
$cacheItem->get()
->willReturn($this->testCachedJwks1);
$cache = $this->prophesize(CacheItemPoolInterface::class);
$cache->getItem($this->testJwksUriKey)
->willReturn($cacheItem->reveal());
$cache->save(Argument::any())
->willReturn(true);
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->prophesize(ClientInterface::class)->reveal(),
$this->prophesize(RequestFactoryInterface::class)->reveal(),
$cache->reveal()
);
$this->assertInstanceOf(Key::class, $cachedKeySet['foo']);
$this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm());
}
public function testCachedKeyIdRefresh()
{
$cacheItem = $this->prophesize(CacheItemInterface::class);
$cacheItem->isHit()
->shouldBeCalledOnce()
->willReturn(true);
$cacheItem->get()
->shouldBeCalledOnce()
->willReturn($this->testCachedJwks1);
$cacheItem->set(Argument::any())
->shouldBeCalledOnce()
->will(function () {
return $this;
});
$cache = $this->prophesize(CacheItemPoolInterface::class);
$cache->getItem($this->testJwksUriKey)
->shouldBeCalledOnce()
->willReturn($cacheItem->reveal());
$cache->save(Argument::any())
->shouldBeCalledOnce()
->willReturn(true);
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->getMockHttpClient($this->testJwks2), // updated JWK
$this->getMockHttpFactory(),
$cache->reveal()
);
$this->assertInstanceOf(Key::class, $cachedKeySet['foo']);
$this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm());
$this->assertInstanceOf(Key::class, $cachedKeySet['bar']);
$this->assertSame('bar', $cachedKeySet['bar']->getAlgorithm());
}
public function testKeyIdIsCachedFromPreviousFormat()
{
$cacheItem = $this->prophesize(CacheItemInterface::class);
$cacheItem->isHit()
->willReturn(true);
$cacheItem->get()
->willReturn($this->testJwks1);
$cache = $this->prophesize(CacheItemPoolInterface::class);
$cache->getItem($this->testJwksUriKey)
->willReturn($cacheItem->reveal());
$cache->save(Argument::any())
->willReturn(true);
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->prophesize(ClientInterface::class)->reveal(),
$this->prophesize(RequestFactoryInterface::class)->reveal(),
$cache->reveal()
);
$this->assertInstanceOf(Key::class, $cachedKeySet['foo']);
$this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm());
}
public function testCachedKeyIdRefreshFromPreviousFormat()
{
$cacheItem = $this->prophesize(CacheItemInterface::class);
$cacheItem->isHit()
->shouldBeCalledOnce()
->willReturn(true);
$cacheItem->get()
->shouldBeCalledOnce()
->willReturn($this->testJwks1);
$cacheItem->set(Argument::any())
->shouldBeCalledOnce()
->will(function () {
return $this;
});
$cache = $this->prophesize(CacheItemPoolInterface::class);
$cache->getItem($this->testJwksUriKey)
->shouldBeCalledOnce()
->willReturn($cacheItem->reveal());
$cache->save(Argument::any())
->shouldBeCalledOnce()
->willReturn(true);
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->getMockHttpClient($this->testJwks2), // updated JWK
$this->getMockHttpFactory(),
$cache->reveal()
);
$this->assertInstanceOf(Key::class, $cachedKeySet['foo']);
$this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm());
$this->assertInstanceOf(Key::class, $cachedKeySet['bar']);
$this->assertSame('bar', $cachedKeySet['bar']->getAlgorithm());
}
public function testCacheItemWithExpiresAfter()
{
$expiresAfter = 10;
$cacheItem = $this->prophesize(CacheItemInterface::class);
$cacheItem->isHit()
->shouldBeCalledOnce()
->willReturn(false);
$cacheItem->set(Argument::any())
->shouldBeCalledOnce()
->will(function () {
return $this;
});
$cacheItem->expiresAfter($expiresAfter)
->shouldBeCalledOnce()
->will(function () {
return $this;
});
$cache = $this->prophesize(CacheItemPoolInterface::class);
$cache->getItem($this->testJwksUriKey)
->shouldBeCalledOnce()
->willReturn($cacheItem->reveal());
$cache->save(Argument::any())
->shouldBeCalledOnce();
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->getMockHttpClient($this->testJwks1),
$this->getMockHttpFactory(),
$cache->reveal(),
$expiresAfter
);
$this->assertInstanceOf(Key::class, $cachedKeySet['foo']);
$this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm());
}
public function testJwtVerify()
{
$privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem');
$payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')];
$msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1');
// format the cached value to match the expected format
$cachedJwks = [];
$rsaKeySet = file_get_contents(__DIR__ . '/data/rsa-jwkset.json');
foreach (json_decode($rsaKeySet, true)['keys'] as $k => $v) {
$cachedJwks[$v['kid']] = $v;
}
$cacheItem = $this->prophesize(CacheItemInterface::class);
$cacheItem->isHit()
->willReturn(true);
$cacheItem->get()
->willReturn($cachedJwks);
$cache = $this->prophesize(CacheItemPoolInterface::class);
$cache->getItem($this->testJwksUriKey)
->willReturn($cacheItem->reveal());
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->prophesize(ClientInterface::class)->reveal(),
$this->prophesize(RequestFactoryInterface::class)->reveal(),
$cache->reveal()
);
$result = JWT::decode($msg, $cachedKeySet);
$this->assertSame('foo', $result->sub);
}
public function testRateLimit()
{
// We request the key 11 times, HTTP should only be called 10 times
$shouldBeCalledTimes = 10;
// Instantiate the cached key set
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->getMockHttpClient($this->testJwks1, $shouldBeCalledTimes),
$this->getMockHttpFactory($shouldBeCalledTimes),
new TestMemoryCacheItemPool(),
10, // expires after seconds
true // enable rate limiting
);
$invalidKid = 'invalidkey';
for ($i = 0; $i < 10; $i++) {
$this->assertFalse(isset($cachedKeySet[$invalidKid]));
}
// The 11th time does not call HTTP
$this->assertFalse(isset($cachedKeySet[$invalidKid]));
}
public function testRateLimitWithExpiresAfter()
{
// We request the key 17 times, HTTP should only be called 15 times
$shouldBeCalledTimes = 10;
$cachedTimes = 2;
$afterExpirationTimes = 5;
$totalHttpTimes = $shouldBeCalledTimes + $afterExpirationTimes;
$cachePool = new TestMemoryCacheItemPool();
// Instantiate the cached key set
$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$this->getMockHttpClient($this->testJwks1, $totalHttpTimes),
$this->getMockHttpFactory($totalHttpTimes),
$cachePool,
10, // expires after seconds
true // enable rate limiting
);
// Set the rate limit cache to expire after 1 second
$cacheItem = $cachePool->getItem('jwksratelimitjwkshttpsjwk.uri');
$cacheItem->set([
'expiry' => new \DateTime('+1 second', new \DateTimeZone('UTC')),
'callsPerMinute' => 0,
]);
$cacheItem->expiresAfter(1);
$cachePool->save($cacheItem);
$invalidKid = 'invalidkey';
for ($i = 0; $i < $shouldBeCalledTimes; $i++) {
$this->assertFalse(isset($cachedKeySet[$invalidKid]));
}
// The next calls do not call HTTP
for ($i = 0; $i < $cachedTimes; $i++) {
$this->assertFalse(isset($cachedKeySet[$invalidKid]));
}
sleep(1); // wait for cache to expire
// These calls DO call HTTP because the cache has expired
for ($i = 0; $i < $afterExpirationTimes; $i++) {
$this->assertFalse(isset($cachedKeySet[$invalidKid]));
}
}
/**
* @dataProvider provideFullIntegration
*/
public function testFullIntegration(string $jwkUri): void
{
if (!class_exists(\GuzzleHttp\Psr7\HttpFactory::class)) {
self::markTestSkipped('Guzzle 7 only');
}
// Create cache and http objects
$cache = new TestMemoryCacheItemPool();
$http = new \GuzzleHttp\Client();
$factory = new \GuzzleHttp\Psr7\HttpFactory();
// Determine "kid" dynamically, because these constantly change
$response = $http->get($jwkUri);
$json = (string) $response->getBody();
$keys = json_decode($json, true);
$kid = $keys['keys'][0]['kid'] ?? null;
$this->assertNotNull($kid);
// Instantiate the cached key set
$cachedKeySet = new CachedKeySet(
$jwkUri,
$http,
$factory,
$cache
);
$this->assertArrayHasKey($kid, $cachedKeySet);
$key = $cachedKeySet[$kid];
$this->assertInstanceOf(Key::class, $key);
$this->assertSame($keys['keys'][0]['alg'], $key->getAlgorithm());
}
public function provideFullIntegration()
{
return [
[$this->googleRsaUri],
[$this->googleEcUri, 'LYyP2g']
];
}
private function getMockHttpClient($testJwks, int $timesCalled = 1)
{
$body = $this->prophesize('Psr\Http\Message\StreamInterface');
$body->__toString()
->shouldBeCalledTimes($timesCalled)
->willReturn($testJwks);
$response = $this->prophesize('Psr\Http\Message\ResponseInterface');
$response->getBody()
->shouldBeCalledTimes($timesCalled)
->willReturn($body->reveal());
$response->getStatusCode()
->shouldBeCalledTimes($timesCalled)
->willReturn(200);
$http = $this->prophesize(ClientInterface::class);
$http->sendRequest(Argument::any())
->shouldBeCalledTimes($timesCalled)
->willReturn($response->reveal());
return $http->reveal();
}
private function getMockHttpFactory(int $timesCalled = 1)
{
$request = $this->prophesize('Psr\Http\Message\RequestInterface');
$factory = $this->prophesize(RequestFactoryInterface::class);
$factory->createRequest('GET', $this->testJwksUri)
->shouldBeCalledTimes($timesCalled)
->willReturn($request->reveal());
return $factory->reveal();
}
private function getMockEmptyCache()
{
$cacheItem = $this->prophesize(CacheItemInterface::class);
$cacheItem->isHit()
->shouldBeCalledOnce()
->willReturn(false);
$cacheItem->set(Argument::any())
->will(function () {
return $this;
});
$cache = $this->prophesize(CacheItemPoolInterface::class);
$cache->getItem($this->testJwksUriKey)
->shouldBeCalledOnce()
->willReturn($cacheItem->reveal());
$cache->save(Argument::any())
->willReturn(true);
return $cache->reveal();
}
}
/**
* A cache item pool
*/
final class TestMemoryCacheItemPool implements CacheItemPoolInterface
{
private $items;
private $deferredItems;
public function getItem($key): CacheItemInterface
{
$item = current($this->getItems([$key]));
$item->expiresAt(null); // mimic symfony cache behavior
return $item;
}
public function getItems(array $keys = []): iterable
{
$items = [];
foreach ($keys as $key) {
$items[$key] = $this->hasItem($key) ? clone $this->items[$key] : new TestMemoryCacheItem($key);
}
return $items;
}
public function hasItem($key): bool
{
return isset($this->items[$key]) && $this->items[$key]->isHit();
}
public function clear(): bool
{
$this->items = [];
$this->deferredItems = [];
return true;
}
public function deleteItem($key): bool
{
return $this->deleteItems([$key]);
}
public function deleteItems(array $keys): bool
{
foreach ($keys as $key) {
unset($this->items[$key]);
}
return true;
}
public function save(CacheItemInterface $item): bool
{
$this->items[$item->getKey()] = $item;
return true;
}
public function saveDeferred(CacheItemInterface $item): bool
{
$this->deferredItems[$item->getKey()] = $item;
return true;
}
public function commit(): bool
{
foreach ($this->deferredItems as $item) {
$this->save($item);
}
$this->deferredItems = [];
return true;
}
}
/**
* A cache item.
*/
final class TestMemoryCacheItem implements CacheItemInterface
{
private $key;
private $value;
private $expiration;
private $isHit = false;
public function __construct(string $key)
{
$this->key = $key;
}
public function getKey(): string
{
return $this->key;
}
public function get(): mixed
{
return $this->isHit() ? $this->value : null;
}
public function isHit(): bool
{
if (!$this->isHit) {
return false;
}
if ($this->expiration === null) {
return true;
}
return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp();
}
public function set(mixed $value): static
{
$this->isHit = true;
$this->value = $value;
return $this;
}
public function expiresAt($expiration): static
{
$this->expiration = $expiration;
return $this;
}
public function expiresAfter($time): static
{
$this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S"));
return $this;
}
protected function currentTime()
{
return new \DateTime('now', new \DateTimeZone('UTC'));
}
}
+232
View File
@@ -0,0 +1,232 @@
<?php
namespace Firebase\JWT;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use UnexpectedValueException;
class JWKTest extends TestCase
{
private static $keys;
private static $privKey1;
private static $privKey2;
public function testMissingKty()
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('JWK must contain a "kty" parameter');
$badJwk = ['kid' => 'foo'];
$keys = JWK::parseKeySet(['keys' => [$badJwk]]);
}
public function testInvalidAlgorithm()
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('No supported algorithms found in JWK Set');
$badJwk = ['kty' => 'BADTYPE', 'alg' => 'RSA256'];
$keys = JWK::parseKeySet(['keys' => [$badJwk]]);
}
public function testParsePrivateKey()
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('RSA private keys are not supported');
$jwkSet = json_decode(
file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
true
);
$jwkSet['keys'][0]['d'] = 'privatekeyvalue';
JWK::parseKeySet($jwkSet);
}
public function testParsePrivateKeyWithoutAlg()
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('JWK must contain an "alg" parameter');
$jwkSet = json_decode(
file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
true
);
unset($jwkSet['keys'][0]['alg']);
JWK::parseKeySet($jwkSet);
}
public function testParsePrivateKeyWithoutAlgWithDefaultAlgParameter()
{
$jwkSet = json_decode(
file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
true
);
unset($jwkSet['keys'][0]['alg']);
$jwks = JWK::parseKeySet($jwkSet, 'foo');
$this->assertSame('foo', $jwks['jwk1']->getAlgorithm());
}
public function testParseKeyWithEmptyDValue()
{
$jwkSet = json_decode(
file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
true
);
// empty or null values are ok
$jwkSet['keys'][0]['d'] = null;
$keys = JWK::parseKeySet($jwkSet);
$this->assertTrue(\is_array($keys));
}
public function testParseJwkKeySet()
{
$jwkSet = json_decode(
file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
true
);
$keys = JWK::parseKeySet($jwkSet);
$this->assertTrue(\is_array($keys));
$this->assertArrayHasKey('jwk1', $keys);
self::$keys = $keys;
}
public function testParseJwkKey_empty()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('JWK must not be empty');
JWK::parseKeySet(['keys' => [[]]]);
}
public function testParseJwkKeySet_empty()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('JWK Set did not contain any keys');
JWK::parseKeySet(['keys' => []]);
}
/**
* @depends testParseJwkKeySet
*/
public function testDecodeByJwkKeySetTokenExpired()
{
$privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem');
$payload = ['exp' => strtotime('-1 hour')];
$msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1');
$this->expectException(ExpiredException::class);
JWT::decode($msg, self::$keys);
}
/**
* @dataProvider provideDecodeByJwkKeySet
*/
public function testDecodeByJwkKeySet($pemFile, $jwkFile, $alg, $keyId)
{
$privKey1 = file_get_contents(__DIR__ . '/data/' . $pemFile);
$payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')];
$msg = JWT::encode($payload, $privKey1, $alg, $keyId);
$jwkSet = json_decode(
file_get_contents(__DIR__ . '/data/' . $jwkFile),
true
);
$keys = JWK::parseKeySet($jwkSet);
$result = JWT::decode($msg, $keys);
$this->assertSame('foo', $result->sub);
}
public function provideDecodeByJwkKeySet()
{
return [
['rsa1-private.pem', 'rsa-jwkset.json', 'RS256', 'jwk1'],
['ecdsa256-private.pem', 'ec-jwkset.json', 'ES256', 'jwk1'],
['ecdsa384-private.pem', 'ec-jwkset.json', 'ES384', 'jwk4'],
['ed25519-1.sec', 'ed25519-jwkset.json', 'EdDSA', 'jwk1'],
];
}
/**
* @depends testParseJwkKeySet
*/
public function testDecodeByMultiJwkKeySet()
{
$privKey2 = file_get_contents(__DIR__ . '/data/rsa2-private.pem');
$payload = ['sub' => 'bar', 'exp' => strtotime('+10 seconds')];
$msg = JWT::encode($payload, $privKey2, 'RS256', 'jwk2');
$result = JWT::decode($msg, self::$keys);
$this->assertSame('bar', $result->sub);
}
public function testDecodeByOctetJwkKeySet()
{
$jwkSet = json_decode(
file_get_contents(__DIR__ . '/data/octet-jwkset.json'),
true
);
$keys = JWK::parseKeySet($jwkSet);
$payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')];
foreach ($keys as $keyId => $key) {
$msg = JWT::encode($payload, $key->getKeyMaterial(), $key->getAlgorithm(), $keyId);
$result = JWT::decode($msg, $keys);
$this->assertSame('foo', $result->sub);
}
}
public function testOctetJwkMissingK()
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('k not set');
$badJwk = ['kty' => 'oct', 'alg' => 'HS256'];
$keys = JWK::parseKeySet(['keys' => [$badJwk]]);
}
public function testParseKey()
{
// Use a known module and exponent, and ensure it parses as expected
$jwk = [
'alg' => 'RS256',
'kty' => 'RSA',
'n' => 'hsYvCPtkUV7SIxwkOkJsJfhwV_CMdXU5i0UmY2QEs-Pa7v0-0y-s4EjEDtsQ8Yow6hc670JhkGBcMzhU4DtrqNGROXebyOse5FX0m0UvWo1qXqNTf28uBKB990mY42Icr8sGjtOw8ajyT9kufbmXi3eZKagKpG0TDGK90oBEfoGzCxoFT87F95liNth_GoyU5S8-G3OqIqLlQCwxkI5s-g2qvg_aooALfh1rhvx2wt4EJVMSrdnxtPQSPAtZBiw5SwCnVglc6OnalVNvAB2JArbqC9GAzzz9pApAk28SYg5a4hPiPyqwRv-4X1CXEK8bO5VesIeRX0oDf7UoM-pVAw',
'use' => 'sig',
'e' => 'AQAB',
'kid' => '838c06c62046c2d948affe137dd5310129f4d5d1'
];
$key = JWK::parseKey($jwk);
$this->assertNotNull($key);
$openSslKey = $key->getKeyMaterial();
$pubKey = openssl_pkey_get_public($openSslKey);
$keyData = openssl_pkey_get_details($pubKey);
$expectedPublicKey = <<<EOF
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhsYvCPtkUV7SIxwkOkJs
JfhwV/CMdXU5i0UmY2QEs+Pa7v0+0y+s4EjEDtsQ8Yow6hc670JhkGBcMzhU4Dtr
qNGROXebyOse5FX0m0UvWo1qXqNTf28uBKB990mY42Icr8sGjtOw8ajyT9kufbmX
i3eZKagKpG0TDGK90oBEfoGzCxoFT87F95liNth/GoyU5S8+G3OqIqLlQCwxkI5s
+g2qvg/aooALfh1rhvx2wt4EJVMSrdnxtPQSPAtZBiw5SwCnVglc6OnalVNvAB2J
ArbqC9GAzzz9pApAk28SYg5a4hPiPyqwRv+4X1CXEK8bO5VesIeRX0oDf7UoM+pV
AwIDAQAB
-----END PUBLIC KEY-----
EOF;
$this->assertEquals($expectedPublicKey, $keyData['key']);
}
}
+778
View File
@@ -0,0 +1,778 @@
<?php
namespace Firebase\JWT;
use DomainException;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use stdClass;
use TypeError;
use UnexpectedValueException;
class JWTTest extends TestCase
{
private Key $hmacKey;
public function setUp(): void
{
$this->hmacKey = $this->generateHmac256();
}
public function testUrlSafeCharacters()
{
$encoded = JWT::encode(['message' => 'f?'], $this->hmacKey->getKeyMaterial(), 'HS256');
$expected = new stdClass();
$expected->message = 'f?';
$this->assertEquals($expected, JWT::decode($encoded, $this->hmacKey));
}
public function testMalformedUtf8StringsFail()
{
$this->expectException(DomainException::class);
JWT::encode(['message' => pack('c', 128)], $this->hmacKey->getKeyMaterial(), 'HS256');
}
public function testInvalidKeyOpensslSignFail()
{
$this->expectException(DomainException::class);
JWT::sign('message', 'invalid key', 'openssl');
}
public function testMalformedJsonThrowsException()
{
$this->expectException(DomainException::class);
JWT::jsonDecode('this is not valid JSON string');
}
public function testExpiredToken()
{
$this->expectException(ExpiredException::class);
$payload = [
'message' => 'abc',
'exp' => time() - 20, // time in the past
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
JWT::decode($encoded, $this->hmacKey);
}
public function testBeforeValidTokenWithNbf()
{
$this->expectException(BeforeValidException::class);
$payload = [
'message' => 'abc',
'nbf' => time() + 20, // time in the future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
JWT::decode($encoded, $this->hmacKey);
}
public function testBeforeValidTokenWithIat()
{
$this->expectException(BeforeValidException::class);
$payload = [
'message' => 'abc',
'iat' => time() + 20, // time in the future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
JWT::decode($encoded, $this->hmacKey);
}
public function testValidToken()
{
$payload = [
'message' => 'abc',
'exp' => time() + JWT::$leeway + 20, // time in the future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$decoded = JWT::decode($encoded, $this->hmacKey);
$this->assertSame($decoded->message, 'abc');
}
/**
* @runInSeparateProcess
*/
public function testValidTokenWithLeeway()
{
JWT::$leeway = 60;
$payload = [
'message' => 'abc',
'exp' => time() - 20, // time in the past
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$decoded = JWT::decode($encoded, $this->hmacKey);
$this->assertSame($decoded->message, 'abc');
}
/**
* @runInSeparateProcess
*/
public function testExpiredTokenWithLeeway()
{
$this->expectException(ExpiredException::class);
JWT::$leeway = 60;
$payload = [
'message' => 'abc',
'exp' => time() - 70, // time far in the past
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$decoded = JWT::decode($encoded, $this->hmacKey);
$this->assertSame($decoded->message, 'abc');
}
public function testExpiredExceptionPayload()
{
$this->expectException(ExpiredException::class);
$payload = [
'message' => 'abc',
'exp' => time() - 100, // time in the past
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
try {
JWT::decode($encoded, $this->hmacKey);
} catch (ExpiredException $e) {
$exceptionPayload = (array) $e->getPayload();
$this->assertEquals($exceptionPayload, $payload);
throw $e;
}
}
/**
* @runInSeparateProcess
*/
public function testExpiredExceptionTimestamp()
{
$this->expectException(ExpiredException::class);
JWT::$timestamp = 98765;
$payload = [
'message' => 'abc',
'exp' => 1234,
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
try {
JWT::decode($encoded, $this->hmacKey);
} catch (ExpiredException $e) {
$exTimestamp = $e->getTimestamp();
$this->assertSame(98765, $exTimestamp);
throw $e;
}
}
public function testBeforeValidExceptionPayload()
{
$this->expectException(BeforeValidException::class);
$payload = [
'message' => 'abc',
'iat' => time() + 100, // time in the future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
try {
JWT::decode($encoded, $this->hmacKey);
} catch (BeforeValidException $e) {
$exceptionPayload = (array) $e->getPayload();
$this->assertEquals($exceptionPayload, $payload);
throw $e;
}
}
public function testValidTokenWithNbf()
{
$payload = [
'message' => 'abc',
'iat' => time(),
'exp' => time() + 20, // time in the future
'nbf' => time() - 20
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$decoded = JWT::decode($encoded, $this->hmacKey);
$this->assertSame($decoded->message, 'abc');
}
/**
* @runInSeparateProcess
*/
public function testValidTokenWithNbfLeeway()
{
JWT::$leeway = 60;
$payload = [
'message' => 'abc',
'nbf' => time() + 20, // not before in near (leeway) future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$decoded = JWT::decode($encoded, $this->hmacKey);
$this->assertSame($decoded->message, 'abc');
}
/**
* @runInSeparateProcess
*/
public function testInvalidTokenWithNbfLeeway()
{
JWT::$leeway = 60;
$payload = [
'message' => 'abc',
'nbf' => time() + 65, // not before too far in future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$this->expectException(BeforeValidException::class);
$this->expectExceptionMessage('Cannot handle token with nbf prior to');
JWT::decode($encoded, $this->hmacKey);
}
public function testValidTokenWithNbfIgnoresIat()
{
$payload = [
'message' => 'abc',
'nbf' => time() - 20, // time in the future
'iat' => time() + 20, // time in the past
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$decoded = JWT::decode($encoded, $this->hmacKey);
$this->assertEquals('abc', $decoded->message);
}
public function testValidTokenWithNbfMicrotime()
{
$payload = [
'message' => 'abc',
'nbf' => microtime(true), // use microtime
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$decoded = JWT::decode($encoded, $this->hmacKey);
$this->assertEquals('abc', $decoded->message);
}
public function testInvalidTokenWithNbfMicrotime()
{
$this->expectException(BeforeValidException::class);
$this->expectExceptionMessage('Cannot handle token with nbf prior to');
$payload = [
'message' => 'abc',
'nbf' => microtime(true) + 20, // use microtime in the future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
JWT::decode($encoded, $this->hmacKey);
}
/**
* @runInSeparateProcess
*/
public function testValidTokenWithIatLeeway()
{
JWT::$leeway = 60;
$payload = [
'message' => 'abc',
'iat' => time() + 20, // issued in near (leeway) future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$decoded = JWT::decode($encoded, $this->hmacKey);
$this->assertSame($decoded->message, 'abc');
}
/**
* @runInSeparateProcess
*/
public function testInvalidTokenWithIatLeeway()
{
JWT::$leeway = 60;
$payload = [
'message' => 'abc',
'iat' => time() + 65, // issued too far in future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$this->expectException(BeforeValidException::class);
$this->expectExceptionMessage('Cannot handle token with iat prior to');
JWT::decode($encoded, $this->hmacKey);
}
public function testValidTokenWithIatMicrotime()
{
$payload = [
'message' => 'abc',
'iat' => microtime(true), // use microtime
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$decoded = JWT::decode($encoded, $this->hmacKey);
$this->assertEquals('abc', $decoded->message);
}
public function testInvalidTokenWithIatMicrotime()
{
$this->expectException(BeforeValidException::class);
$this->expectExceptionMessage('Cannot handle token with iat prior to');
$payload = [
'message' => 'abc',
'iat' => microtime(true) + 20, // use microtime in the future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
JWT::decode($encoded, $this->hmacKey);
}
public function testInvalidToken()
{
$encodeKey = $this->generateHmac256();
$decodeKey = $this->generateHmac256();
$payload = [
'message' => 'abc',
'exp' => time() + 20, // time in the future
];
$encoded = JWT::encode($payload, $encodeKey->getKeyMaterial(), $encodeKey->getAlgorithm());
$this->expectException(SignatureInvalidException::class);
JWT::decode($encoded, $decodeKey);
}
public function testNullKeyFails()
{
$payload = [
'message' => 'abc',
'exp' => time() + JWT::$leeway + 20, // time in the future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$this->expectException(TypeError::class);
JWT::decode($encoded, new Key(null, 'HS256'));
}
public function testEmptyKeyFails()
{
$payload = [
'message' => 'abc',
'exp' => time() + JWT::$leeway + 20, // time in the future
];
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
$this->expectException(InvalidArgumentException::class);
JWT::decode($encoded, new Key('', 'HS256'));
}
public function testKIDChooser()
{
$keys = [
'0' => $this->generateHmac256(),
'1' => $this->generateHmac256(),
'2' => $this->generateHmac256()
];
$msg = JWT::encode(['message' => 'abc'], $keys['0']->getKeyMaterial(), 'HS256', '0');
$decoded = JWT::decode($msg, $keys);
$expected = new stdClass();
$expected->message = 'abc';
$this->assertEquals($decoded, $expected);
}
public function testArrayAccessKIDChooser()
{
$keys = [
'0' => $this->generateHmac256(),
'1' => $this->generateHmac256(),
'2' => $this->generateHmac256()
];
$msg = JWT::encode(['message' => 'abc'], $keys['0']->getKeyMaterial(), 'HS256', '0');
$decoded = JWT::decode($msg, $keys);
$expected = new stdClass();
$expected->message = 'abc';
$this->assertEquals($decoded, $expected);
}
public function testNoneAlgorithm()
{
$msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256');
$this->expectException(UnexpectedValueException::class);
JWT::decode($msg, new Key($this->hmacKey->getKeyMaterial(), 'none'));
}
public function testIncorrectAlgorithm()
{
$msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256');
$this->expectException(UnexpectedValueException::class);
// TODO: Generate proper RS256 key
JWT::decode($msg, new Key($this->hmacKey->getKeyMaterial(), 'RS256'));
}
public function testEmptyAlgorithm()
{
$msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256');
$this->expectException(InvalidArgumentException::class);
JWT::decode($msg, new Key($this->hmacKey->getKeyMaterial(), ''));
}
public function testAdditionalHeaders()
{
$msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256', null, ['cty' => 'test-eit;v=1']);
$expected = new stdClass();
$expected->message = 'abc';
$this->assertEquals(JWT::decode($msg, $this->hmacKey), $expected);
}
public function testInvalidSegmentCount()
{
$this->expectException(UnexpectedValueException::class);
JWT::decode('brokenheader.brokenbody', $this->hmacKey);
}
public function testInvalidSignatureEncoding()
{
$msg = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx';
$this->expectException(UnexpectedValueException::class);
JWT::decode($msg, $this->hmacKey);
}
public function testHSEncodeDecode()
{
$msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256');
$expected = new stdClass();
$expected->message = 'abc';
$this->assertEquals(JWT::decode($msg, $this->hmacKey), $expected);
}
public function testRSEncodeDecode()
{
$privKey = openssl_pkey_new([
'digest_alg' => 'sha256',
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA
]);
$msg = JWT::encode(['message' => 'abc'], $privKey, 'RS256');
$pubKey = openssl_pkey_get_details($privKey);
$pubKey = $pubKey['key'];
$decoded = JWT::decode($msg, new Key($pubKey, 'RS256'));
$expected = new stdClass();
$expected->message = 'abc';
$this->assertEquals($decoded, $expected);
}
public function testEdDsaEncodeDecode()
{
$keyPair = sodium_crypto_sign_keypair();
$privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
$payload = ['foo' => 'bar'];
$msg = JWT::encode($payload, $privKey, 'EdDSA');
$pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
$decoded = JWT::decode($msg, new Key($pubKey, 'EdDSA'));
$this->assertSame('bar', $decoded->foo);
}
public function testInvalidEdDsaEncodeDecode()
{
$keyPair = sodium_crypto_sign_keypair();
$privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
$payload = ['foo' => 'bar'];
$msg = JWT::encode($payload, $privKey, 'EdDSA');
// Generate a different key.
$keyPair = sodium_crypto_sign_keypair();
$pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
$this->expectException(SignatureInvalidException::class);
JWT::decode($msg, new Key($pubKey, 'EdDSA'));
}
public function testRSEncodeDecodeWithPassphrase()
{
$privateKey = openssl_pkey_get_private(
file_get_contents(__DIR__ . '/data/rsa-with-passphrase.pem'),
'passphrase'
);
$jwt = JWT::encode(['message' => 'abc'], $privateKey, 'RS256');
$keyDetails = openssl_pkey_get_details($privateKey);
$pubKey = $keyDetails['key'];
$decoded = JWT::decode($jwt, new Key($pubKey, 'RS256'));
$expected = new stdClass();
$expected->message = 'abc';
$this->assertEquals($decoded, $expected);
}
public function testDecodesEmptyArrayAsObject()
{
$key = 'yma6Hq4XQegCVND8ef23OYgxSrC3IKqk';
$payload = [];
$jwt = JWT::encode($payload, $key, 'HS256');
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
$this->assertEquals((object) $payload, $decoded);
}
public function testDecodesArraysInJWTAsArray()
{
$key = 'yma6Hq4XQegCVND8ef23OYgxSrC3IKqk';
$payload = ['foo' => [1, 2, 3]];
$jwt = JWT::encode($payload, $key, 'HS256');
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
$this->assertSame($payload['foo'], $decoded->foo);
}
/**
* @runInSeparateProcess
* @dataProvider provideEncodeDecode
*/
public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg)
{
$privateKey = file_get_contents($privateKeyFile);
$payload = ['foo' => 'bar'];
$encoded = JWT::encode($payload, $privateKey, $alg);
// Verify decoding succeeds
$publicKey = file_get_contents($publicKeyFile);
$decoded = JWT::decode($encoded, new Key($publicKey, $alg));
$this->assertSame('bar', $decoded->foo);
}
public function provideEncodeDecode()
{
return [
[__DIR__ . '/data/ecdsa-private.pem', __DIR__ . '/data/ecdsa-public.pem', 'ES256'],
[__DIR__ . '/data/ecdsa384-private.pem', __DIR__ . '/data/ecdsa384-public.pem', 'ES384'],
[__DIR__ . '/data/rsa1-private.pem', __DIR__ . '/data/rsa1-public.pub', 'RS512'],
[__DIR__ . '/data/ed25519-1.sec', __DIR__ . '/data/ed25519-1.pub', 'EdDSA'],
[__DIR__ . '/data/secp256k1-private.pem', __DIR__ . '/data/secp256k1-public.pem', 'ES256K'],
];
}
public function testEncodeDecodeWithOpenSSLAsymmetricKey()
{
$pem = file_get_contents(__DIR__ . '/data/rsa1-public.pub');
$keyMaterial = openssl_pkey_get_public($pem);
$privateKey = file_get_contents(__DIR__ . '/data/rsa1-private.pem');
$payload = ['foo' => 'bar'];
$encoded = JWT::encode($payload, $privateKey, 'RS512');
// Verify decoding succeeds
$decoded = JWT::decode($encoded, new Key($keyMaterial, 'RS512'));
$this->assertSame('bar', $decoded->foo);
}
public function testGetHeaders()
{
$payload = [
'message' => 'abc',
'exp' => time() + JWT::$leeway + 20, // time in the future
];
$headers = new stdClass();
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
JWT::decode($encoded, $this->hmacKey, $headers);
$this->assertEquals($headers->typ, 'JWT');
$this->assertEquals($headers->alg, 'HS256');
}
public function testAdditionalHeaderOverrides()
{
$msg = JWT::encode(
['message' => 'abc'],
$this->hmacKey->getKeyMaterial(),
'HS256',
'my_key_id',
[
'cty' => 'test-eit;v=1',
'typ' => 'JOSE', // override type header
'kid' => 'not_my_key_id', // should not override $key param
'alg' => 'BAD', // should not override $alg param
]
);
$headers = new stdClass();
JWT::decode($msg, $this->hmacKey, $headers);
$this->assertEquals('test-eit;v=1', $headers->cty, 'additional field works');
$this->assertEquals('JOSE', $headers->typ, 'typ override works');
$this->assertEquals('my_key_id', $headers->kid, 'key param not overridden');
$this->assertEquals('HS256', $headers->alg, 'alg param not overridden');
}
public function testDecodeExpectsIntegerIat()
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Payload iat must be a number');
$payload = JWT::encode(['iat' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256');
JWT::decode($payload, $this->hmacKey);
}
public function testDecodeExpectsIntegerNbf()
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Payload nbf must be a number');
$payload = JWT::encode(['nbf' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256');
JWT::decode($payload, $this->hmacKey);
}
public function testDecodeExpectsIntegerExp()
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Payload exp must be a number');
$payload = JWT::encode(['exp' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256');
JWT::decode($payload, $this->hmacKey);
}
public function testRsaKeyLengthValidationThrowsException(): void
{
$this->expectException(DomainException::class);
$this->expectExceptionMessage('Provided key is too short');
// Generate an RSA key that is smaller than the 2048-bit minimum
$shortRsaKey = openssl_pkey_new([
'private_key_bits' => 1024,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
self::assertNotFalse($shortRsaKey, 'Failed to generate a short RSA key for testing.');
$payload = ['message' => 'abc'];
JWT::encode($payload, $shortRsaKey, 'RS256');
}
/** @dataProvider provideHmac */
public function testHmacKeyLengthValidationThrowsExceptionEncode(string $alg, int $minLength): void
{
$this->expectException(DomainException::class);
$this->expectExceptionMessage('Provided key is too short');
$tooShortKeyBytes = str_repeat('b', $minLength - 1);
$payload = ['message' => 'abc'];
JWT::encode($payload, $tooShortKeyBytes, $alg);
}
/** @dataProvider provideHmac */
public function testHmacKeyLengthValidationThrowsExceptionDecode(string $alg, int $minLength): void
{
$this->expectException(DomainException::class);
$this->expectExceptionMessage('Provided key is too short');
$tooShortKeyBytes = str_repeat('b', $minLength - 1);
$payload = ['message' => 'abc'];
$validKeyBytes = str_repeat('b', $minLength);
$encoded = JWT::encode($payload, $validKeyBytes, $alg);
JWT::decode($encoded, new Key($tooShortKeyBytes, $alg));
}
/** @dataProvider provideHmac */
public function testHmacKeyLengthValidationPassesWithCorrectLength(string $alg, int $minLength): void
{
$payload = ['message' => 'test hmac length'];
// Test with a key that is exactly the required length
$minKeyBytes = str_repeat('b', $minLength);
$encoded48 = JWT::encode($payload, $minKeyBytes, $alg);
$decoded48 = JWT::decode($encoded48, new Key($minKeyBytes, $alg));
$this->assertEquals($payload['message'], $decoded48->message);
// Test with a key that is longer than the required length
$largeKeyBytes = str_repeat('c', $minLength * 2); // Longer than min bytes
$encoded64 = JWT::encode($payload, $largeKeyBytes, $alg);
$decoded64 = JWT::decode($encoded64, new Key($largeKeyBytes, $alg));
$this->assertEquals($payload['message'], $decoded64->message);
}
public function provideHmac()
{
return [
['HS384', 48],
['HS256', 32],
];
}
public function testEdDsaHandlesBase64UrlKeys()
{
if (!\extension_loaded('sodium')) {
$this->markTestSkipped('libsodium is not available');
}
// Generate a deterministic Ed25519 keypair using a specific seed. The byte "\xfb"
// translates to '+' and '/' in standard base64, which become '-' and '_' in Base64URL.
// This guarantees our keys will contain the URL-safe characters that get incorrectly
// stripped by base64_decode().
$seed = str_repeat("\xfb", 32);
$keyPair = sodium_crypto_sign_seed_keypair($seed);
$secretKey = sodium_crypto_sign_secretkey($keyPair);
$publicKey = sodium_crypto_sign_publickey($keyPair);
// Convert the raw keys to Base64URL encoded strings
$secretKeyB64u = JWT::urlsafeB64Encode($secretKey);
$publicKeyB64u = JWT::urlsafeB64Encode($publicKey);
// Ensure our test keys actually contain the characters that get
// incorrectly stripped by a standard base64_decode().
$this->assertTrue(strpos($secretKeyB64u, '-') !== false || strpos($secretKeyB64u, '_') !== false);
$this->assertTrue(strpos($publicKeyB64u, '-') !== false || strpos($publicKeyB64u, '_') !== false);
// Test Encoding
$token = JWT::encode(['issue' => 596], $secretKeyB64u, 'EdDSA');
$this->assertIsString($token);
// Test Decoding
$decoded = JWT::decode($token, new Key($publicKeyB64u, 'EdDSA'));
$this->assertSame(596, $decoded->issue);
}
/** @dataProvider provideEcKeyInvalidLength */
public function testEcKeyLengthValidationThrowsExceptionEncode(string $keyFile, string $alg): void
{
$this->expectException(DomainException::class);
$this->expectExceptionMessage('Provided key is too short');
$tooShortEcKey = file_get_contents(__DIR__ . '/data/' . $keyFile);
$payload = ['message' => 'abc'];
JWT::encode($payload, $tooShortEcKey, $alg);
}
public function testEcKeyLengthValidationThrowsExceptionDecode(): void
{
$this->expectException(DomainException::class);
$this->expectExceptionMessage('Provided key is too short');
$payload = ['message' => 'abc'];
$validEcKeyBytes = file_get_contents(__DIR__ . '/data/ecdsa384-private.pem');
$encoded = JWT::encode($payload, $validEcKeyBytes, 'ES256');
$tooShortEcKey = file_get_contents(__DIR__ . '/data/ecdsa192-public.pem');
JWT::decode($encoded, new Key($tooShortEcKey, 'ES256'));
}
/** @dataProvider provideEcKey */
public function testEcKeyLengthValidationPassesWithCorrectLength(
string $privateKeyFile,
string $publicKeyFile,
string $alg
): void {
$payload = ['message' => 'test hmac length'];
// Test with a key that is the required length
$privateKeyBytes = file_get_contents(__DIR__ . '/data/' . $privateKeyFile);
$encoded48 = JWT::encode($payload, $privateKeyBytes, $alg);
$publicKeyBytes = file_get_contents(__DIR__ . '/data/' . $publicKeyFile);
$decoded48 = JWT::decode($encoded48, new Key($publicKeyBytes, $alg));
$this->assertEquals($payload['message'], $decoded48->message);
}
public function provideEcKeyInvalidLength()
{
return [
['ecdsa192-private.pem', 'ES256'],
['ecdsa-private.pem', 'ES384'],
];
}
public function provideEcKey()
{
return [
['ecdsa-private.pem', 'ecdsa-public.pem', 'ES256'],
['ecdsa384-private.pem', 'ecdsa384-public.pem', 'ES384'],
];
}
private function generateHmac256(): Key
{
return new Key(random_bytes(32), 'HS256');
}
}
+201
View File
@@ -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);
}
};
}
}
+40
View File
@@ -0,0 +1,40 @@
{
"keys": [
{
"kty": "EC",
"use": "sig",
"crv": "P-256",
"kid": "jwk1",
"x": "ALXnvdCvbBx35J2bozBkIFHPT747KiYioLK4JquMhZU",
"y": "fAt_rGPqS95Ytwdluh4TNWTmj9xkcAbKGBRpP5kuGBk",
"alg": "ES256"
},
{
"kty": "EC",
"use": "sig",
"crv": "P-256",
"kid": "jwk2",
"x": "mQa0q5FvxPRujxzFazQT1Mo2YJJzuKiXU3svOJ41jhw",
"y": "jAz7UwIl2oOFk06kj42ZFMOXmGMFUGjKASvyYtibCH0",
"alg": "ES256"
},
{
"kty": "EC",
"use": "sig",
"crv": "secp256k1",
"kid": "jwk3",
"x": "EFpwNuP322bU3WP1DtJgx67L0CUV1MxNixqPVMH2L9Q",
"y": "_fSTbijIJjpsqL16cIEvxxf3MaYMY8MbqEq066yV9ls",
"alg": "ES256K"
},
{
"kty": "EC",
"use": "sig",
"crv": "P-384",
"kid": "jwk4",
"x": "FhXXcyKmWkTkdVbWYYU3dtJqpJ0JmLGftEdNzUEFEKSU5MlnLr_FjcneszvXAqEB",
"y": "M4veJF_dO_zhFk44bh_ELXbp0_nn9QaViVtQpuTvpu29eefx6PfUMqX0K--IS4NQ",
"alg": "ES384"
}
]
}
+18
View File
@@ -0,0 +1,18 @@
-----BEGIN EC PARAMETERS-----
MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////
/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6
k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+
kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK
fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz
ucrC/GMlUQIBAQ==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIIBaAIBAQQgyP9e7yS1tjpXa0l6o+80dbSxuMcqx3lUg0n2OT9AmiuggfowgfcC
AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////
MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr
vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE
axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W
K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8
YyVRAgEBoUQDQgAE2klp6aX6y5kAir3EWQt0QAeapTW+db/9fD65KAoDzVajtThx
PVLEf1CufcfTxMQAQPM3wkZhu0NjlWFetcMdcQ==
-----END EC PRIVATE KEY-----
+9
View File
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA
AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA////
///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd
NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5
RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA
//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABNpJaeml+suZAIq9xFkLdEAH
mqU1vnW//Xw+uSgKA81Wo7U4cT1SxH9Qrn3H08TEAEDzN8JGYbtDY5VhXrXDHXE=
-----END PUBLIC KEY-----
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MF8CAQEEGPRkK7lK/9FuZ3BE8ZX+dlHavL22Q9CN2KAKBggqhkjOPQMBAaE0AzIA
BL4pM50YcLq/I9Y8T+C+fwoOtwRW8zdV6yQmG9fD8zWaAs28+UxHeK8VD7THatbp
wg==
-----END EC PRIVATE KEY-----
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEvikznRhwur8j1jxP4L5/Cg63BFbz
N1XrJCYb18PzNZoCzbz5TEd4rxUPtMdq1unC
-----END PUBLIC KEY-----
@@ -0,0 +1,4 @@
-----BEGIN PRIVATE KEY-----
MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCD0KvVxLJEzRBQmcEXf
D2okKCNoUwZY8fc1/1Z4aJuJdg==
-----END PRIVATE KEY-----
@@ -0,0 +1,6 @@
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDBQJuwafREZ1494Fm2MTVXuZbWXVAOwIAxGhyLdc3CChzi0FVXZq8e6
65oR0Qq9Jv2gBwYFK4EEACKhZANiAAQWFddzIqZaROR1VtZhhTd20mqknQmYsZ+0
R03NQQUQpJTkyWcuv8WNyd6zO9cCoQEzi94kX907/OEWTjhuH8QtdunT+ef1BpWJ
W1Cm5O+m7b155/Ho99QypfQr74hLg1A=
-----END EC PRIVATE KEY-----
@@ -0,0 +1,5 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEFhXXcyKmWkTkdVbWYYU3dtJqpJ0JmLGf
tEdNzUEFEKSU5MlnLr/FjcneszvXAqEBM4veJF/dO/zhFk44bh/ELXbp0/nn9QaV
iVtQpuTvpu29eefx6PfUMqX0K++IS4NQ
-----END PUBLIC KEY-----
+1
View File
@@ -0,0 +1 @@
uOSJMhbKSG4V5xUHS7B9YHmVg/1yVd+G+Io6oBFhSfY=
+1
View File
@@ -0,0 +1 @@
i4eTKkWNIISKumdk3v90cPDrY/g8WRTJWy7DmGDsdzC45IkyFspIbhXnFQdLsH1geZWD/XJV34b4ijqgEWFJ9g==
+11
View File
@@ -0,0 +1,11 @@
{
"keys": [
{
"kid": "jwk1",
"alg": "EdDSA",
"kty": "OKP",
"crv": "Ed25519",
"x": "uOSJMhbKSG4V5xUHS7B9YHmVg_1yVd-G-Io6oBFhSfY"
}
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"keys": [
{
"kty": "oct",
"alg": "HS256",
"kid": "jwk1",
"k": "xUNfVvQ-WdmXB9qp6qK0SrG-yKW4AJqmcSP66Gm2TrE"
},
{
"kty": "oct",
"alg": "HS384",
"kid": "jwk2",
"k": "z7990HoD72QDX9JKqeQc3l7EtXutco72j2YulZMjeakFVDbFGXGDFG4awOF7eu9l"
},
{
"kty": "oct",
"alg": "HS512",
"kid": "jwk3",
"k": "EmYGSDG5W1UjkPIL7LelG-QMVtsXn7bz5lUxBrkqq3kdFEzkLWVGrXKpZxRe7YcApCe0d4s9lXRQtn5Nzaf49w"
}
]
}
+18
View File
@@ -0,0 +1,18 @@
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "jwk1",
"n": "0Ttga33B1yX4w77NbpKyNYDNSVCo8j-RlZaZ9tI-KfkV1d-tfsvI9ZPAheP11FoN52ceBaY5ltelHW-IKwCfyT0orLdsxLgowaXki9woF1Azvcg2JVxQLv9aVjjAvy3CZFIG_EeN7J3nsyCXGnu1yMEbnvkWxA88__Q6HQ2K9wqfApkQ0LNlsK0YHz_sfjHNvRKxnbAJk7D5fUhZunPZXOPHXFgA5SvLvMaNIXduMKJh4OMfuoLdJowXJAR9j31Mqz_is4FMhm_9Mq7vZZ-uF09htRvIR8tRY28oJuW1gKWyg7cQQpnjHgFyG3XLXWAeXclWqyh_LfjyHQjrYhyeFw",
"alg": "RS256"
},
{
"kty": "RSA",
"e": "AQAB",
"kid": "jwk2",
"n": "pXi2o6AnNhwL30MaK_nuDHi2fxZHVen7Xwk0bjLGlHYpq3mSvXm2HBA-zR41vQCbHkYGsDpsyDhIXLBDTbSa7ue7D1ZqYdv5YLIS33zdX9GtUHfFHc6zYgXAU9ziWeyTzVn7icAbjxqcgT2xKNuGK7Zf2ZJ053rr-dxjAE-SjX4SG0WWUhwPjxlr1etF7mEurhHweuSdZYl36g39o9BtTBVfS87io2MwdIRsnL3w8ulgXRVRWjv-vvcuhMS_y6zGbzOC55Yr23sb4h2PSll32bgyglEIsGgHqjOdyjuUzl0t6jh86DHzbu9h-u1iihX8EI8t7CBbizbPPyHQygp-rQ",
"alg": "RS256"
}
]
}
@@ -0,0 +1,42 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,D8AA4EC8D8B5883F09ACB308FB026C94
ixqo1+NTlkiUHUa1bucqHNQ4nca4cnaosK8Lauftc0WuyqNVE+NL/zxdiUKN+Qi1
bhEkvMKgbqTMzPFUws3wNoPEI/eaoGYHTl4nAX79JWjJ8/DWY+VVp5IFSzNEM1MP
NMWaivfBGhd8W9kBmpOJpQjwePFk7hdLkEvSngGRhDmEV046cWr7I+koYKEG/oW9
53NnDNKPKLPkzM/Me4GQ6nXarqUPoIn/c3qFLgkhkzLJ/Lu21wnYx46RasXJv3oq
xT3nRIat/Q6jtlzLLwvo+lpvJW3G+rKqjEi76Av7Cm1TkHQFW9CGsnQ4ZDn427KL
FGojP6QG5RjLI6IiAHgt0lnzOwtjbF1RQBHIWedC4Rufb5u92SRKJ2PvidB/suJ7
SR/PPA2XpK22QBMccO9yjNh4ZZIV6I2cqv3BlKR2RFU0552sEQr6usxPfFhExIRR
1eiaLtIupo3uEC5e2fBKtI7D3T7WztUagTw0vSgoxhTdc8XIoT0prV91SvyEEZMw
r5LSRW4BvyCekG9FFyIS2fOWabgxmm16siNErTbS2RS3GGimX0v5O+KIN9ho1uAY
5U865amaOZshop1YYixtDJL27JhpkODhwXrB1lNQOCdi64CV2r8VlVPNg6TWZlli
vJ6agKvWmTppy07ovbBRB+llmW6eGtjwEmAvMaWNgkFNkgDF/wBnDi91tx8/8UL7
XQy0VZz128FtpJC0G0Z/5HmxqoEJAwk1+EzO5tgnfc+2wIONGCV2ISph0efVtPui
xOP6geaeSrxBxL/BUcIX5DMfN6hsvz+Pb8bE9WT2+fz/ySCJhkfraC/vHbs3wn3R
CICCvYtR803ku53GCgsEZ8vmIxMb1D0mJnfWvSQtDBqF8XwhL6m5ShbeaMLkbmZ9
0WLWj0zAcOkbX4TXLGVaRPRs9HjSEr7+jEVHO6OeKj60rG9M3NVmfig7J8ta/zvy
1Hk4MiucTsp0I+G/hx8dqoV4x1kTyn0WZMfD8PxnbPdPvbhG2tQn7xkZykgtvK5y
s1fMbvqVGDfn5PmLeSwYkyohYZGbiwV5UldhwdG/ZnagI1KPuJ10OYBOSLCcGufY
aUHmIFSvfYqbN5YfKsMCZmmrX73pDcXOWGWto8nTFS9f4RlQI0Vh25xJqqinD6Vu
ErP7+XxDZCLqKew/xfq1fcKoiCOA/9IK5meyjRV4Z5QxkgTeBmyNVt/MW+6QIJJJ
WoBWqpootxtb28YN2RuD0byEIyP8pmoyN3MOPYGNSia8PAQgIL6z71Ju2SejXADy
ybirbrS0Y/oZABqhLK5qDdCYe4O5zp/lbwWn2Gfp3G3xKUxfBWi4f/VQwUjUbYCz
XHFVLpDY1mMPaedo7Tp5ZGN4OHwIlpspcwI0U9TYac0AxZuSBPjE8YqJ2qJBhaiZ
dEE7CxwkSLLxXVEPp7+VO6CORZfYXXaRcpTAZfrDURSI5RkT8n6LElnrzFBilb0q
ejlKaLD4MLlvlc/NWl/w+TfuN/iGlQm02Ul8yysG1b0w8R+seMNHhHS4+848ZRBd
HoWUuYiYXZTJxmP5dc0f/Sul672YSFp7rGzt9+7hFV6WrkAFNxETkQ8cbA/GiGvz
Kvv1GI/Ms8YymAJWiv7skFTmGcHMbjxga2EOBtSfYF5mwV3KEMPRpYsn1nw6U99E
NuWFqT+p4VqVSgmeG11zwM7v+Vt3RZDUggZWDsNKGA9V9ciAlHY2U7CH6xihBCfh
suHNuzVC1nAwi/ZrhfJXMKk+hJ8o+5dXSTYp4eCEGh4U2l3pmmAejZenJqlGs0Ke
MYHQRCk5zaB5myRYuvwtUSbZ/BaVVFSQz758Vw4HxKFLnvudtAXktu3sTcOgYKQS
PaiolwZFr4lp3h74BlIYcYrREmBJv6Hy1lOLAd5X3iExiy+DdRJWkuNd+19Cblq3
ePHf2Mgp+AElxmyA6EHyt86v3E2mL7xNAUUVrNb3UJTi6io5KASMVmNbrGGJksC7
y3OuHaq1RM7UvR/eI38nI2YOckoKDgkhHPtaXkIpO9jX3RRYlA2uzsf44DU7etyc
c2ICApYVdKruR/pmFN45pcIPy6x3zU34fkRTMf1F3yShJzr8Ntd/C63Km8XaganW
2AVWuuvOJXjMqu4+OXzrIqObFFp6naqv1E+O8/14i8k4VW3dmWnMM7eq9FvqQdiM
y0tBbGILfAVYtjh59r+CKeqRoq7o/xlsVin1Vxn74K6uYUphjXWUhMXXStGZ8sBc
QDOPTanB+LPBeCAgQFQe1SHrGiIognXT0g2WFqW8DrxwTqr6olPoMF6LU01vqT0+
HVZtczjk0LvDLZm8bsCDGBPDdbDI/tfvXncP5PgEtFSTUiRy+zryy82AF4rJhudH
-----END RSA PRIVATE KEY-----
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA0Ttga33B1yX4w77NbpKyNYDNSVCo8j+RlZaZ9tI+KfkV1d+t
fsvI9ZPAheP11FoN52ceBaY5ltelHW+IKwCfyT0orLdsxLgowaXki9woF1Azvcg2
JVxQLv9aVjjAvy3CZFIG/EeN7J3nsyCXGnu1yMEbnvkWxA88//Q6HQ2K9wqfApkQ
0LNlsK0YHz/sfjHNvRKxnbAJk7D5fUhZunPZXOPHXFgA5SvLvMaNIXduMKJh4OMf
uoLdJowXJAR9j31Mqz/is4FMhm/9Mq7vZZ+uF09htRvIR8tRY28oJuW1gKWyg7cQ
QpnjHgFyG3XLXWAeXclWqyh/LfjyHQjrYhyeFwIDAQABAoIBAHMqdJsWAGEVNIVB
+792HYNXnydQr32PwemNmLeD59WglgU/9jZJoxaROjI4VLKK0wZg+uRvJ1nA3tCB
+Hh7Anh5Im9XExaAq2ZTkqXtC2AxtBktH6iW1EfaI/Y7jNRuMoaXo+Ku3A62p7cw
JBvepiOXL0Xko0RNguz7mBUvxCLPhYhzn7qCbM8uXLcjsXq/YhWQwQmtMqv0sd3W
Hy+8Jb2c18sqDeZIBne4dWD6qPClPEOsrq9gPTkl0DjbT27oVc2u1p4HMNm5BJIh
u3rMSxnZHUd7Axj1FgyLIOHl63UhaiaA1aPe/fLiVIGOA1jBZrpbnjgqDy9Uxyn6
eydbiwECgYEA9mtRydz22idyUOlBCDXk+vdGBvFAucNYaNNUAXUJ2wfPmdGgFCA7
g5eQG8JC6J/FU+2AfIuz6LGr7SxMBYcsWGjFAzGqs/sJib+zzN1dPUSRn4uJNFit
51yQzPgBqHS6S/XBi6YAODeZDl9jiPl3FxxucqLY5NstqZFXbE0SjIECgYEA2V3r
7xnRAK1krY1+zkPof4kcBmjqOXjnl/oRxlXP65lEXmyNJwm/ulOIko9mElWRs8CG
AxSWKaab9Gk6lc8MHjVRbuW52RGLGKq1mp6ENr4d3IBOfrNsTvD3gtNEN1JFLeF1
jIbSsrbi2txr7VZ06Irac0C/ytro0QDOUoXkvpcCgYA8O0EzmToRWsD7e/g0XJAK
s/Q+8CtE/LWYccc/z+7HxeH9lBqPsM07Pgmwb0xRdfQSrqPQTYl9ICiJAWHXnBG/
zmQRgstZ0MulCuGU+qq2thLuL3oq/F4NhjeykhA9r8J1nK1hSAMXuqdDtxcqPOfa
E03/4UQotFY181uuEiytgQKBgHQT+gjHqptH/XnJFCymiySAXdz2bg6fCF5aht95
t/1C7gXWxlJQnHiuX0KVHZcw5wwtBePjPIWlmaceAtE5rmj7ZC9qsqK/AZ78mtql
SEnLoTq9si1rN624dRUCKW25m4Py4MlYvm/9xovGJkSqZOhCLoJZ05JK8QWb/pKH
Oi6lAoGBAOUN6ICpMQvzMGPgIbgS0H/gvRTnpAEs59vdgrkhlCII4tzfgvBQlVae
hRcdM6GTMq5pekBPKu45eanIzwVc88P6coT4qiWYKk2jYoLBa0UV3xEAuqBMymrj
X4nLcSbZtO0tcDGMfMpWF2JGYOEJQNetPozL/ICGVFyIO8yzXm8U
-----END RSA PRIVATE KEY-----
+9
View File
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ttga33B1yX4w77NbpKy
NYDNSVCo8j+RlZaZ9tI+KfkV1d+tfsvI9ZPAheP11FoN52ceBaY5ltelHW+IKwCf
yT0orLdsxLgowaXki9woF1Azvcg2JVxQLv9aVjjAvy3CZFIG/EeN7J3nsyCXGnu1
yMEbnvkWxA88//Q6HQ2K9wqfApkQ0LNlsK0YHz/sfjHNvRKxnbAJk7D5fUhZunPZ
XOPHXFgA5SvLvMaNIXduMKJh4OMfuoLdJowXJAR9j31Mqz/is4FMhm/9Mq7vZZ+u
F09htRvIR8tRY28oJuW1gKWyg7cQQpnjHgFyG3XLXWAeXclWqyh/LfjyHQjrYhye
FwIDAQAB
-----END PUBLIC KEY-----
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEApXi2o6AnNhwL30MaK/nuDHi2fxZHVen7Xwk0bjLGlHYpq3mS
vXm2HBA+zR41vQCbHkYGsDpsyDhIXLBDTbSa7ue7D1ZqYdv5YLIS33zdX9GtUHfF
Hc6zYgXAU9ziWeyTzVn7icAbjxqcgT2xKNuGK7Zf2ZJ053rr+dxjAE+SjX4SG0WW
UhwPjxlr1etF7mEurhHweuSdZYl36g39o9BtTBVfS87io2MwdIRsnL3w8ulgXRVR
Wjv+vvcuhMS/y6zGbzOC55Yr23sb4h2PSll32bgyglEIsGgHqjOdyjuUzl0t6jh8
6DHzbu9h+u1iihX8EI8t7CBbizbPPyHQygp+rQIDAQABAoIBACF25kj1LLjutx/x
7CsUoqX3C8Fr+gVQCrxPmkDnF+4Sb570OU8EfGX0ix7kiy2sH7LhqpydVD6x00Cb
jSD785F5YAVcDqu31xlNKi/0irjEKO7rKfw7P2AFlb3gIA7bn5CaMBrNtUUdtqUU
mu2OZ/YTLhNMYUQnQe4IOiVn8lWW5D4Kje/RlLRRdGn8voXaD5BnOwZNXAxjdXqM
RxyXRG74tLKyfe3W8xTL8uhlKCNHjsdtUg9IZdnKT7I3DJPobpqgC3fUuC/IbfGf
MPK1aiu067/3DdgonC2ZWqFeKLJqtUa7z0pSQaZeDa1iiUuRivfqKYEBovFre6ni
1qHkp8ECgYEA089VnKc74NRGVbIs0VtQGprNhkl47eBq6jhTlG3hfaFF4VuDiZiu
wT8enlbhlbDb/gM0CDr9tkfDs7R4exNnhSVvn2PT8b1mhonOAeE466y/4YBA0d9x
gj0wF2vjH/bsVNBe6MBrIx12R2tBKTZ7tbCzgJRszSZqkrK7sljTlaUCgYEAx/54
G3Yd3ULqGIG/JA7w/QEYitgjwAUSJ+eLU+iqlIjo/njAJwJ/kixqaI3Jzcl+kYmp
yNIXNNaJUz8c0M/QsuqvQjLnHkF0FOZUrdyVseU2mSbI6DhAGsPJEtAOep/61vyz
uJSu0z34gQ6bNrKdqfkA7XIQRNJ1r0qQXrVLRmkCgYB2/UYaIDTaREZTBCp7XnHs
0ERfiUz/TZCijgweGXCQ1BXe2TtXBEhAVcZMq4BFSLr9wyzq5sD7Muu1O9BnS+pe
+T3w6/L4Hi/HqwjpM253r2+ILjW78Wvh/5/RuJE6tsvjhb+bv+UwL+/vhUhw76Ol
2WOt+zP4N/ms+e3J7m7G5QKBgQCmasN65nC3WyT8u4pX8O7rOOw5LN2ivRV8ixnO
+r5m1v46MjSCwXtyIO9yjPmt+csOQ+U6LEgPOa4PzWanAyaAmvS3OzBCZui3M2qn
OfR+kWM7UaDAS35cRyqcMvC5bUIHf0P1hhNryBdvHL5fZ4X2mDMDYnTTL+WptXwo
sucucQKBgAGHzi5+ZRwffhpZiYVR/lA6zvqyekAncJZwGe2UVDL0axTumX1NPdin
2mOnVuvKVvJkisyKTIQzFk6ClQEyiArO4+t7zhUbg5Crh8q6nObRo2R2NcP8o0Iq
BRIwPgaG/WlEvZ6zqlHQ0qH7WoL4HnRG5uyLOuzRIkjasYmZdfR8
-----END RSA PRIVATE KEY-----
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgC8ouvv1ZOmOjh5Nbwx6i
3b35wWN+OEkW2hzm3BKAQJ2hRANCAAT9nYGLVP6Unm/LXOoyWhsKpalffMSr3EHV
iUE8gVmj2/atnPkblx38Yj6bC3z1urERAB+JqgpWOAKaWcEYCUuO
-----END PRIVATE KEY-----
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE/Z2Bi1T+lJ5vy1zqMlobCqWpX3zEq9xB
1YlBPIFZo9v2rZz5G5cd/GI+mwt89bqxEQAfiaoKVjgCmlnBGAlLjg==
-----END PUBLIC KEY-----