update packages
This commit is contained in:
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "dabestaniha/authenticate-bridge",
|
"name": "dabestaniha/authenticate-bridge",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.1",
|
||||||
"firebase/php-jwt": "^6.11"
|
"firebase/php-jwt": "^7.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
Generated
+11
-10
@@ -4,20 +4,20 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "775143d1bcec2aacbb576adc03b110ae",
|
"content-hash": "20c98790ea3a0bf0f64fa300f73ec329",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "firebase/php-jwt",
|
"name": "firebase/php-jwt",
|
||||||
"version": "v6.11.1",
|
"version": "v7.0.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/firebase/php-jwt.git",
|
"url": "https://github.com/googleapis/php-jwt.git",
|
||||||
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
|
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
|
"url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380",
|
||||||
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
|
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"guzzlehttp/guzzle": "^7.4",
|
"guzzlehttp/guzzle": "^7.4",
|
||||||
|
"phpfastcache/phpfastcache": "^9.2",
|
||||||
"phpspec/prophecy-phpunit": "^2.0",
|
"phpspec/prophecy-phpunit": "^2.0",
|
||||||
"phpunit/phpunit": "^9.5",
|
"phpunit/phpunit": "^9.5",
|
||||||
"psr/cache": "^2.0||^3.0",
|
"psr/cache": "^2.0||^3.0",
|
||||||
@@ -64,10 +65,10 @@
|
|||||||
"php"
|
"php"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/firebase/php-jwt/issues",
|
"issues": "https://github.com/googleapis/php-jwt/issues",
|
||||||
"source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
|
"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": [],
|
"packages-dev": [],
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "^8.2"
|
"php": "^8.1"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.6.0"
|
||||||
|
|||||||
Vendored
+4
-1
@@ -14,7 +14,10 @@ if (PHP_VERSION_ID < 50600) {
|
|||||||
echo $err;
|
echo $err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new RuntimeException($err);
|
trigger_error(
|
||||||
|
$err,
|
||||||
|
E_USER_ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
require_once __DIR__ . '/composer/autoload_real.php';
|
require_once __DIR__ . '/composer/autoload_real.php';
|
||||||
|
|||||||
+1
-19
@@ -26,12 +26,6 @@ use Composer\Semver\VersionParser;
|
|||||||
*/
|
*/
|
||||||
class InstalledVersions
|
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
|
* @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
|
* @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;
|
self::$installedIsLocalDir = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private static function getSelfDir()
|
|
||||||
{
|
|
||||||
if (self::$selfDir === null) {
|
|
||||||
self::$selfDir = strtr(__DIR__, '\\', '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::$selfDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array[]
|
* @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[]}>}>
|
* @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;
|
$copiedLocalDir = false;
|
||||||
|
|
||||||
if (self::$canGetVendors) {
|
if (self::$canGetVendors) {
|
||||||
$selfDir = self::getSelfDir();
|
$selfDir = strtr(__DIR__, '\\', '/');
|
||||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||||
$vendorDir = strtr($vendorDir, '\\', '/');
|
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||||
|
|||||||
Vendored
+11
-10
@@ -2,17 +2,17 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "firebase/php-jwt",
|
"name": "firebase/php-jwt",
|
||||||
"version": "v6.11.1",
|
"version": "v7.0.5",
|
||||||
"version_normalized": "6.11.1.0",
|
"version_normalized": "7.0.5.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/firebase/php-jwt.git",
|
"url": "https://github.com/googleapis/php-jwt.git",
|
||||||
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
|
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
|
"url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380",
|
||||||
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
|
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"guzzlehttp/guzzle": "^7.4",
|
"guzzlehttp/guzzle": "^7.4",
|
||||||
|
"phpfastcache/phpfastcache": "^9.2",
|
||||||
"phpspec/prophecy-phpunit": "^2.0",
|
"phpspec/prophecy-phpunit": "^2.0",
|
||||||
"phpunit/phpunit": "^9.5",
|
"phpunit/phpunit": "^9.5",
|
||||||
"psr/cache": "^2.0||^3.0",
|
"psr/cache": "^2.0||^3.0",
|
||||||
@@ -30,9 +31,9 @@
|
|||||||
"ext-sodium": "Support EdDSA (Ed25519) signatures",
|
"ext-sodium": "Support EdDSA (Ed25519) signatures",
|
||||||
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
|
"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",
|
"type": "library",
|
||||||
"installation-source": "dist",
|
"installation-source": "source",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Firebase\\JWT\\": "src"
|
"Firebase\\JWT\\": "src"
|
||||||
@@ -61,8 +62,8 @@
|
|||||||
"php"
|
"php"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/firebase/php-jwt/issues",
|
"issues": "https://github.com/googleapis/php-jwt/issues",
|
||||||
"source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
|
"source": "https://github.com/googleapis/php-jwt/tree/v7.0.5"
|
||||||
},
|
},
|
||||||
"install-path": "../firebase/php-jwt"
|
"install-path": "../firebase/php-jwt"
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+5
-5
@@ -3,7 +3,7 @@
|
|||||||
'name' => 'dabestaniha/authenticate-bridge',
|
'name' => 'dabestaniha/authenticate-bridge',
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => 'dev-main',
|
'version' => 'dev-main',
|
||||||
'reference' => '89a7eed84730a7731c7c74cfba53878925b4ccbe',
|
'reference' => '9dbdfa7e21bb4c0fca40b1f84265c0ad559c133c',
|
||||||
'type' => 'library',
|
'type' => 'library',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
@@ -13,16 +13,16 @@
|
|||||||
'dabestaniha/authenticate-bridge' => array(
|
'dabestaniha/authenticate-bridge' => array(
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => 'dev-main',
|
'version' => 'dev-main',
|
||||||
'reference' => '89a7eed84730a7731c7c74cfba53878925b4ccbe',
|
'reference' => '9dbdfa7e21bb4c0fca40b1f84265c0ad559c133c',
|
||||||
'type' => 'library',
|
'type' => 'library',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
'dev_requirement' => false,
|
'dev_requirement' => false,
|
||||||
),
|
),
|
||||||
'firebase/php-jwt' => array(
|
'firebase/php-jwt' => array(
|
||||||
'pretty_version' => 'v6.11.1',
|
'pretty_version' => 'v7.0.5',
|
||||||
'version' => '6.11.1.0',
|
'version' => '7.0.5.0',
|
||||||
'reference' => 'd1e91ecf8c598d073d0995afa8cd5c75c6e19e66',
|
'reference' => '47ad26bab5e7c70ae8a6f08ed25ff83631121380',
|
||||||
'type' => 'library',
|
'type' => 'library',
|
||||||
'install_path' => __DIR__ . '/../firebase/php-jwt',
|
'install_path' => __DIR__ . '/../firebase/php-jwt',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
|
|||||||
Vendored
+5
-4
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
$issues = array();
|
$issues = array();
|
||||||
|
|
||||||
if (!(PHP_VERSION_ID >= 80200)) {
|
if (!(PHP_VERSION_ID >= 80100)) {
|
||||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
|
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($issues) {
|
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;
|
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(
|
trigger_error(
|
||||||
'Composer detected issues in your platform: ' . implode(' ', $issues)
|
'Composer detected issues in your platform: ' . implode(' ', $issues),
|
||||||
|
E_USER_ERROR
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
vendor
|
||||||
|
phpunit.phar
|
||||||
|
phpunit.phar.asc
|
||||||
|
composer.phar
|
||||||
|
composer.lock
|
||||||
|
.phpunit.result.cache
|
||||||
|
.php-cs-fixer.cache
|
||||||
+28
@@ -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__)
|
||||||
|
)
|
||||||
|
;
|
||||||
Vendored
+46
@@ -1,5 +1,51 @@
|
|||||||
# Changelog
|
# 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)
|
## [6.11.1](https://github.com/firebase/php-jwt/compare/v6.11.0...v6.11.1) (2025-04-09)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Vendored
+36
-36
@@ -23,16 +23,16 @@ php env does not have libsodium installed:
|
|||||||
composer require paragonie/sodium_compat
|
composer require paragonie/sodium_compat
|
||||||
```
|
```
|
||||||
|
|
||||||
Example
|
## Example
|
||||||
-------
|
|
||||||
```php
|
```php
|
||||||
use Firebase\JWT\JWT;
|
use Firebase\JWT\JWT;
|
||||||
use Firebase\JWT\Key;
|
use Firebase\JWT\Key;
|
||||||
|
|
||||||
$key = 'example_key';
|
$key = 'example_key_of_sufficient_length';
|
||||||
$payload = [
|
$payload = [
|
||||||
'iss' => 'http://example.org',
|
'iss' => 'example.org',
|
||||||
'aud' => 'http://example.com',
|
'aud' => 'example.com',
|
||||||
'iat' => 1356999524,
|
'iat' => 1356999524,
|
||||||
'nbf' => 1357000000
|
'nbf' => 1357000000
|
||||||
];
|
];
|
||||||
@@ -69,8 +69,9 @@ $decoded_array = (array) $decoded;
|
|||||||
JWT::$leeway = 60; // $leeway in seconds
|
JWT::$leeway = 60; // $leeway in seconds
|
||||||
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
|
$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
|
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.
|
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
|
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
|
```php
|
||||||
use Firebase\JWT\JWT;
|
use Firebase\JWT\JWT;
|
||||||
|
|
||||||
$key = 'example_key';
|
$key = 'example_key_of_sufficient_length';
|
||||||
$payload = [
|
$payload = [
|
||||||
'iss' => 'http://example.org',
|
'iss' => 'example.org',
|
||||||
'aud' => 'http://example.com',
|
'aud' => 'example.com',
|
||||||
'iat' => 1356999524,
|
'iat' => 1356999524,
|
||||||
'nbf' => 1357000000
|
'nbf' => 1357000000
|
||||||
];
|
];
|
||||||
@@ -103,8 +104,9 @@ $decoded = json_decode(base64_decode($headersB64), true);
|
|||||||
|
|
||||||
print_r($decoded);
|
print_r($decoded);
|
||||||
```
|
```
|
||||||
Example with RS256 (openssl)
|
|
||||||
----------------------------
|
## Example with RS256 (openssl)
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Firebase\JWT\JWT;
|
use Firebase\JWT\JWT;
|
||||||
use Firebase\JWT\Key;
|
use Firebase\JWT\Key;
|
||||||
@@ -172,8 +174,7 @@ $decoded_array = (array) $decoded;
|
|||||||
echo "Decode:\n" . print_r($decoded_array, true) . "\n";
|
echo "Decode:\n" . print_r($decoded_array, true) . "\n";
|
||||||
```
|
```
|
||||||
|
|
||||||
Example with a passphrase
|
## Example with a passphrase
|
||||||
-------------------------
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Firebase\JWT\JWT;
|
use Firebase\JWT\JWT;
|
||||||
@@ -186,7 +187,7 @@ $passphrase = '[YOUR_PASSPHRASE]';
|
|||||||
// Can be generated with "ssh-keygen -t rsa -m pem"
|
// Can be generated with "ssh-keygen -t rsa -m pem"
|
||||||
$privateKeyFile = '/path/to/key-with-passphrase.pem';
|
$privateKeyFile = '/path/to/key-with-passphrase.pem';
|
||||||
|
|
||||||
// Create a private key of type "resource"
|
/** @var OpenSSLAsymmetricKey $privateKey */
|
||||||
$privateKey = openssl_pkey_get_private(
|
$privateKey = openssl_pkey_get_private(
|
||||||
file_get_contents($privateKeyFile),
|
file_get_contents($privateKeyFile),
|
||||||
$passphrase
|
$passphrase
|
||||||
@@ -209,8 +210,8 @@ $decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
|
|||||||
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
|
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
|
||||||
```
|
```
|
||||||
|
|
||||||
Example with EdDSA (libsodium and Ed25519 signature)
|
## Example with EdDSA (libsodium and Ed25519 signature)
|
||||||
----------------------------
|
|
||||||
```php
|
```php
|
||||||
use Firebase\JWT\JWT;
|
use Firebase\JWT\JWT;
|
||||||
use Firebase\JWT\Key;
|
use Firebase\JWT\Key;
|
||||||
@@ -238,21 +239,21 @@ echo "Encode:\n" . print_r($jwt, true) . "\n";
|
|||||||
|
|
||||||
$decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA'));
|
$decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA'));
|
||||||
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
|
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
|
||||||
````
|
```
|
||||||
|
|
||||||
|
## Example with multiple keys
|
||||||
|
|
||||||
Example with multiple keys
|
|
||||||
--------------------------
|
|
||||||
```php
|
```php
|
||||||
use Firebase\JWT\JWT;
|
use Firebase\JWT\JWT;
|
||||||
use Firebase\JWT\Key;
|
use Firebase\JWT\Key;
|
||||||
|
|
||||||
// Example RSA keys from previous example
|
// Example RSA keys from previous example
|
||||||
// $privateKey1 = '...';
|
// $privateRsKey = '...';
|
||||||
// $publicKey1 = '...';
|
// $publicRsKey = '...';
|
||||||
|
|
||||||
// Example EdDSA keys from previous example
|
// Example EdDSA keys from previous example
|
||||||
// $privateKey2 = '...';
|
// $privateEcKey = '...';
|
||||||
// $publicKey2 = '...';
|
// $publicEcKey = '...';
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'iss' => 'example.org',
|
'iss' => 'example.org',
|
||||||
@@ -261,14 +262,14 @@ $payload = [
|
|||||||
'nbf' => 1357000000
|
'nbf' => 1357000000
|
||||||
];
|
];
|
||||||
|
|
||||||
$jwt1 = JWT::encode($payload, $privateKey1, 'RS256', 'kid1');
|
$jwt1 = JWT::encode($payload, $privateRsKey, 'RS256', 'kid1');
|
||||||
$jwt2 = JWT::encode($payload, $privateKey2, 'EdDSA', 'kid2');
|
$jwt2 = JWT::encode($payload, $privateEcKey, 'EdDSA', 'kid2');
|
||||||
echo "Encode 1:\n" . print_r($jwt1, true) . "\n";
|
echo "Encode 1:\n" . print_r($jwt1, true) . "\n";
|
||||||
echo "Encode 2:\n" . print_r($jwt2, true) . "\n";
|
echo "Encode 2:\n" . print_r($jwt2, true) . "\n";
|
||||||
|
|
||||||
$keys = [
|
$keys = [
|
||||||
'kid1' => new Key($publicKey1, 'RS256'),
|
'kid1' => new Key($publicRsKey, 'RS256'),
|
||||||
'kid2' => new Key($publicKey2, 'EdDSA'),
|
'kid2' => new Key($publicEcKey, 'EdDSA'),
|
||||||
];
|
];
|
||||||
|
|
||||||
$decoded1 = JWT::decode($jwt1, $keys);
|
$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";
|
echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n";
|
||||||
```
|
```
|
||||||
|
|
||||||
Using JWKs
|
## Using JWKs
|
||||||
----------
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Firebase\JWT\JWK;
|
use Firebase\JWT\JWK;
|
||||||
@@ -291,11 +291,11 @@ $jwks = ['keys' => []];
|
|||||||
|
|
||||||
// JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key
|
// JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key
|
||||||
// objects. Pass this as the second parameter to JWT::decode.
|
// 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.
|
The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI.
|
||||||
This has the following advantages:
|
This has the following advantages:
|
||||||
@@ -315,7 +315,7 @@ $jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk';
|
|||||||
$httpClient = new GuzzleHttp\Client();
|
$httpClient = new GuzzleHttp\Client();
|
||||||
|
|
||||||
// Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory)
|
// 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)
|
// Create a cache item pool (can be any PSR-6 compatible cache item pool)
|
||||||
$cacheItemPool = Phpfastcache\CacheManager::getInstance('files');
|
$cacheItemPool = Phpfastcache\CacheManager::getInstance('files');
|
||||||
@@ -406,8 +406,8 @@ Tests
|
|||||||
Run the tests using phpunit:
|
Run the tests using phpunit:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ pear install PHPUnit
|
$ composer update
|
||||||
$ phpunit --configuration phpunit.xml.dist
|
$ vendor/bin/phpunit -c phpunit.xml.dist
|
||||||
PHPUnit 3.7.10 by Sebastian Bergmann.
|
PHPUnit 3.7.10 by Sebastian Bergmann.
|
||||||
.....
|
.....
|
||||||
Time: 0 seconds, Memory: 2.50Mb
|
Time: 0 seconds, Memory: 2.50Mb
|
||||||
|
|||||||
+2
-1
@@ -37,6 +37,7 @@
|
|||||||
"phpunit/phpunit": "^9.5",
|
"phpunit/phpunit": "^9.5",
|
||||||
"psr/cache": "^2.0||^3.0",
|
"psr/cache": "^2.0||^3.0",
|
||||||
"psr/http-client": "^1.0",
|
"psr/http-client": "^1.0",
|
||||||
"psr/http-factory": "^1.0"
|
"psr/http-factory": "^1.0",
|
||||||
|
"phpfastcache/phpfastcache": "^9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
parameters:
|
||||||
|
level: 7
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
treatPhpDocTypesAsCertain: false
|
||||||
+18
@@ -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
@@ -180,7 +180,8 @@ class CachedKeySet implements ArrayAccess
|
|||||||
$jwksResponse = $this->httpClient->sendRequest($request);
|
$jwksResponse = $this->httpClient->sendRequest($request);
|
||||||
if ($jwksResponse->getStatusCode() !== 200) {
|
if ($jwksResponse->getStatusCode() !== 200) {
|
||||||
throw new UnexpectedValueException(
|
throw new UnexpectedValueException(
|
||||||
\sprintf('HTTP Error: %d %s for URI "%s"',
|
\sprintf(
|
||||||
|
'HTTP Error: %d %s for URI "%s"',
|
||||||
$jwksResponse->getStatusCode(),
|
$jwksResponse->getStatusCode(),
|
||||||
$jwksResponse->getReasonPhrase(),
|
$jwksResponse->getReasonPhrase(),
|
||||||
$this->jwksUri,
|
$this->jwksUri,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ class ExpiredException extends \UnexpectedValueException implements JWTException
|
|||||||
{
|
{
|
||||||
private object $payload;
|
private object $payload;
|
||||||
|
|
||||||
|
private ?int $timestamp = null;
|
||||||
|
|
||||||
public function setPayload(object $payload): void
|
public function setPayload(object $payload): void
|
||||||
{
|
{
|
||||||
$this->payload = $payload;
|
$this->payload = $payload;
|
||||||
@@ -15,4 +17,14 @@ class ExpiredException extends \UnexpectedValueException implements JWTException
|
|||||||
{
|
{
|
||||||
return $this->payload;
|
return $this->payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setTimestamp(int $timestamp): void
|
||||||
|
{
|
||||||
|
$this->timestamp = $timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTimestamp(): ?int
|
||||||
|
{
|
||||||
|
return $this->timestamp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+10
-2
@@ -52,7 +52,7 @@ class JWK
|
|||||||
*
|
*
|
||||||
* @uses parseKey
|
* @uses parseKey
|
||||||
*/
|
*/
|
||||||
public static function parseKeySet(array $jwks, ?string $defaultAlg = null): array
|
public static function parseKeySet(#[\SensitiveParameter] array $jwks, ?string $defaultAlg = null): array
|
||||||
{
|
{
|
||||||
$keys = [];
|
$keys = [];
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ class JWK
|
|||||||
*
|
*
|
||||||
* @uses createPemFromModulusAndExponent
|
* @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)) {
|
if (empty($jwk)) {
|
||||||
throw new InvalidArgumentException('JWK must not be empty');
|
throw new InvalidArgumentException('JWK must not be empty');
|
||||||
@@ -240,6 +240,14 @@ class JWK
|
|||||||
): string {
|
): string {
|
||||||
$mod = JWT::urlsafeB64Decode($n);
|
$mod = JWT::urlsafeB64Decode($n);
|
||||||
$exp = JWT::urlsafeB64Decode($e);
|
$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);
|
$modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
|
||||||
$publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
|
$publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
|
||||||
|
|||||||
Vendored
+118
-40
@@ -31,6 +31,8 @@ class JWT
|
|||||||
private const ASN1_SEQUENCE = 0x10;
|
private const ASN1_SEQUENCE = 0x10;
|
||||||
private const ASN1_BIT_STRING = 0x03;
|
private const ASN1_BIT_STRING = 0x03;
|
||||||
|
|
||||||
|
private const RSA_KEY_MIN_LENGTH = 2048;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When checking nbf, iat or expiration times,
|
* When checking nbf, iat or expiration times,
|
||||||
* we want to provide some extra leeway time to
|
* we want to provide some extra leeway time to
|
||||||
@@ -95,7 +97,7 @@ class JWT
|
|||||||
*/
|
*/
|
||||||
public static function decode(
|
public static function decode(
|
||||||
string $jwt,
|
string $jwt,
|
||||||
$keyOrKeyArray,
|
#[\SensitiveParameter] $keyOrKeyArray,
|
||||||
?stdClass &$headers = null
|
?stdClass &$headers = null
|
||||||
): stdClass {
|
): stdClass {
|
||||||
// Validate JWT
|
// Validate JWT
|
||||||
@@ -127,6 +129,16 @@ class JWT
|
|||||||
if (!$payload instanceof stdClass) {
|
if (!$payload instanceof stdClass) {
|
||||||
throw new UnexpectedValueException('Payload must be a JSON object');
|
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);
|
$sig = static::urlsafeB64Decode($cryptob64);
|
||||||
if (empty($header->alg)) {
|
if (empty($header->alg)) {
|
||||||
throw new UnexpectedValueException('Empty algorithm');
|
throw new UnexpectedValueException('Empty algorithm');
|
||||||
@@ -154,7 +166,7 @@ class JWT
|
|||||||
// token can actually be used. If it's not yet that time, abort.
|
// token can actually be used. If it's not yet that time, abort.
|
||||||
if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) {
|
if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) {
|
||||||
$ex = new BeforeValidException(
|
$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);
|
$ex->setPayload($payload);
|
||||||
throw $ex;
|
throw $ex;
|
||||||
@@ -165,7 +177,7 @@ class JWT
|
|||||||
// correctly used the nbf claim).
|
// correctly used the nbf claim).
|
||||||
if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) {
|
if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) {
|
||||||
$ex = new BeforeValidException(
|
$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);
|
$ex->setPayload($payload);
|
||||||
throw $ex;
|
throw $ex;
|
||||||
@@ -175,6 +187,7 @@ class JWT
|
|||||||
if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
|
if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
|
||||||
$ex = new ExpiredException('Expired token');
|
$ex = new ExpiredException('Expired token');
|
||||||
$ex->setPayload($payload);
|
$ex->setPayload($payload);
|
||||||
|
$ex->setTimestamp($timestamp);
|
||||||
throw $ex;
|
throw $ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,11 +198,11 @@ class JWT
|
|||||||
* Converts and signs a PHP array into a JWT string.
|
* Converts and signs a PHP array into a JWT string.
|
||||||
*
|
*
|
||||||
* @param array<mixed> $payload PHP array
|
* @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',
|
* @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256',
|
||||||
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
|
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
|
||||||
* @param string $keyId
|
* @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
|
* @return string A signed JWT
|
||||||
*
|
*
|
||||||
@@ -198,7 +211,7 @@ class JWT
|
|||||||
*/
|
*/
|
||||||
public static function encode(
|
public static function encode(
|
||||||
array $payload,
|
array $payload,
|
||||||
$key,
|
#[\SensitiveParameter] $key,
|
||||||
string $alg,
|
string $alg,
|
||||||
?string $keyId = null,
|
?string $keyId = null,
|
||||||
?array $head = null
|
?array $head = null
|
||||||
@@ -226,7 +239,7 @@ class JWT
|
|||||||
* Sign a string with a given key and algorithm.
|
* Sign a string with a given key and algorithm.
|
||||||
*
|
*
|
||||||
* @param string $msg The message to sign
|
* @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',
|
* @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256',
|
||||||
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
|
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
|
||||||
*
|
*
|
||||||
@@ -236,7 +249,7 @@ class JWT
|
|||||||
*/
|
*/
|
||||||
public static function sign(
|
public static function sign(
|
||||||
string $msg,
|
string $msg,
|
||||||
$key,
|
#[\SensitiveParameter] $key,
|
||||||
string $alg
|
string $alg
|
||||||
): string {
|
): string {
|
||||||
if (empty(static::$supported_algs[$alg])) {
|
if (empty(static::$supported_algs[$alg])) {
|
||||||
@@ -248,13 +261,19 @@ class JWT
|
|||||||
if (!\is_string($key)) {
|
if (!\is_string($key)) {
|
||||||
throw new InvalidArgumentException('key must be a string when using hmac');
|
throw new InvalidArgumentException('key must be a string when using hmac');
|
||||||
}
|
}
|
||||||
|
self::validateHmacKeyLength($key, $algorithm);
|
||||||
return \hash_hmac($algorithm, $msg, $key, true);
|
return \hash_hmac($algorithm, $msg, $key, true);
|
||||||
case 'openssl':
|
case 'openssl':
|
||||||
$signature = '';
|
$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');
|
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) {
|
if (!$success) {
|
||||||
throw new DomainException('OpenSSL unable to sign data');
|
throw new DomainException('OpenSSL unable to sign data');
|
||||||
}
|
}
|
||||||
@@ -265,20 +284,8 @@ class JWT
|
|||||||
}
|
}
|
||||||
return $signature;
|
return $signature;
|
||||||
case 'sodium_crypto':
|
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 {
|
try {
|
||||||
// The last non-empty line is used as the key.
|
return sodium_crypto_sign_detached($msg, self::validateEdDSAKey($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);
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
throw new DomainException($e->getMessage(), 0, $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 $msg The original message (header and body)
|
||||||
* @param string $signature The original signature
|
* @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
|
* @param string $alg The algorithm
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
@@ -303,7 +310,7 @@ class JWT
|
|||||||
private static function verify(
|
private static function verify(
|
||||||
string $msg,
|
string $msg,
|
||||||
string $signature,
|
string $signature,
|
||||||
$keyMaterial,
|
#[\SensitiveParameter] $keyMaterial,
|
||||||
string $alg
|
string $alg
|
||||||
): bool {
|
): bool {
|
||||||
if (empty(static::$supported_algs[$alg])) {
|
if (empty(static::$supported_algs[$alg])) {
|
||||||
@@ -313,7 +320,15 @@ class JWT
|
|||||||
list($function, $algorithm) = static::$supported_algs[$alg];
|
list($function, $algorithm) = static::$supported_algs[$alg];
|
||||||
switch ($function) {
|
switch ($function) {
|
||||||
case 'openssl':
|
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) {
|
if ($success === 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -325,19 +340,8 @@ class JWT
|
|||||||
'OpenSSL error: ' . \openssl_error_string()
|
'OpenSSL error: ' . \openssl_error_string()
|
||||||
);
|
);
|
||||||
case 'sodium_crypto':
|
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 {
|
try {
|
||||||
// The last non-empty line is used as the key.
|
$key = self::validateEdDSAKey($keyMaterial);
|
||||||
$lines = array_filter(explode("\n", $keyMaterial));
|
|
||||||
$key = base64_decode((string) end($lines));
|
|
||||||
if (\strlen($key) === 0) {
|
|
||||||
throw new DomainException('Key cannot be empty string');
|
|
||||||
}
|
|
||||||
if (\strlen($signature) === 0) {
|
if (\strlen($signature) === 0) {
|
||||||
throw new DomainException('Signature cannot be empty string');
|
throw new DomainException('Signature cannot be empty string');
|
||||||
}
|
}
|
||||||
@@ -350,6 +354,7 @@ class JWT
|
|||||||
if (!\is_string($keyMaterial)) {
|
if (!\is_string($keyMaterial)) {
|
||||||
throw new InvalidArgumentException('key must be a string when using hmac');
|
throw new InvalidArgumentException('key must be a string when using hmac');
|
||||||
}
|
}
|
||||||
|
self::validateHmacKeyLength($keyMaterial, $algorithm);
|
||||||
$hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
|
$hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
|
||||||
return self::constantTimeEquals($hash, $signature);
|
return self::constantTimeEquals($hash, $signature);
|
||||||
}
|
}
|
||||||
@@ -445,7 +450,6 @@ class JWT
|
|||||||
return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
|
return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if an algorithm has been provided for each Key
|
* Determine if an algorithm has been provided for each Key
|
||||||
*
|
*
|
||||||
@@ -457,7 +461,7 @@ class JWT
|
|||||||
* @return Key
|
* @return Key
|
||||||
*/
|
*/
|
||||||
private static function getKey(
|
private static function getKey(
|
||||||
$keyOrKeyArray,
|
#[\SensitiveParameter] $keyOrKeyArray,
|
||||||
?string $kid
|
?string $kid
|
||||||
): Key {
|
): Key {
|
||||||
if ($keyOrKeyArray instanceof Key) {
|
if ($keyOrKeyArray instanceof Key) {
|
||||||
@@ -664,4 +668,78 @@ class JWT
|
|||||||
|
|
||||||
return [$pos, $data];
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
-5
@@ -10,20 +10,19 @@ use TypeError;
|
|||||||
class Key
|
class Key
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial
|
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial
|
||||||
* @param string $algorithm
|
* @param string $algorithm
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private $keyMaterial,
|
#[\SensitiveParameter] private $keyMaterial,
|
||||||
private string $algorithm
|
private string $algorithm
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
!\is_string($keyMaterial)
|
!\is_string($keyMaterial)
|
||||||
&& !$keyMaterial instanceof OpenSSLAsymmetricKey
|
&& !$keyMaterial instanceof OpenSSLAsymmetricKey
|
||||||
&& !$keyMaterial instanceof OpenSSLCertificate
|
&& !$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)) {
|
if (empty($keyMaterial)) {
|
||||||
@@ -46,7 +45,7 @@ class Key
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate
|
* @return string|OpenSSLAsymmetricKey|OpenSSLCertificate
|
||||||
*/
|
*/
|
||||||
public function getKeyMaterial()
|
public function getKeyMaterial()
|
||||||
{
|
{
|
||||||
|
|||||||
+649
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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-----
|
||||||
@@ -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-----
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uOSJMhbKSG4V5xUHS7B9YHmVg/1yVd+G+Io6oBFhSfY=
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
i4eTKkWNIISKumdk3v90cPDrY/g8WRTJWy7DmGDsdzC45IkyFspIbhXnFQdLsH1geZWD/XJV34b4ijqgEWFJ9g==
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"kid": "jwk1",
|
||||||
|
"alg": "EdDSA",
|
||||||
|
"kty": "OKP",
|
||||||
|
"crv": "Ed25519",
|
||||||
|
"x": "uOSJMhbKSG4V5xUHS7B9YHmVg_1yVd-G-Io6oBFhSfY"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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-----
|
||||||
@@ -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-----
|
||||||
@@ -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-----
|
||||||
@@ -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-----
|
||||||
Reference in New Issue
Block a user