1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2024-12-23 02:04:46 +02:00

[Web] update stevenmaguire/oauth2-keycloak and firebase/php-jwt

This commit is contained in:
FreddleSpl0it 2023-05-16 13:31:40 +02:00 committed by DerLinkman
parent a805d3b2e3
commit cee771a3fb
No known key found for this signature in database
GPG Key ID: F109FD97469550A2
16 changed files with 1203 additions and 449 deletions

View File

@ -10,7 +10,7 @@
"mustangostang/spyc": "^0.6.3",
"directorytree/ldaprecord": "^2.4",
"twig/twig": "^3.0",
"stevenmaguire/oauth2-keycloak": "^3.2",
"stevenmaguire/oauth2-keycloak": "^4.0",
"league/oauth2-client": "^2.7"
}
}

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ee35a2bf8c80a87b6825c3e86635f709",
"content-hash": "8f5a147cdb147b935a158b86f47a4747",
"packages": [
{
"name": "bshaffer/oauth2-server-php",
@ -218,25 +218,31 @@
},
{
"name": "firebase/php-jwt",
"version": "v5.5.1",
"version": "v6.5.0",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "83b609028194aa042ea33b5af2d41a7427de80e6"
"reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/83b609028194aa042ea33b5af2d41a7427de80e6",
"reference": "83b609028194aa042ea33b5af2d41a7427de80e6",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/e94e7353302b0c11ec3cfff7180cd0b1743975d2",
"reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
"php": "^7.4||^8.0"
},
"require-dev": {
"phpunit/phpunit": ">=4.8 <=9"
"guzzlehttp/guzzle": "^6.5||^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^1.0||^2.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"type": "library",
@ -269,9 +275,9 @@
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v5.5.1"
"source": "https://github.com/firebase/php-jwt/tree/v6.5.0"
},
"time": "2021-11-08T20:18:51+00:00"
"time": "2023-05-12T15:47:07+00:00"
},
{
"name": "guzzlehttp/guzzle",
@ -1703,26 +1709,27 @@
},
{
"name": "stevenmaguire/oauth2-keycloak",
"version": "3.2.0",
"version": "4.0.0",
"source": {
"type": "git",
"url": "https://github.com/stevenmaguire/oauth2-keycloak.git",
"reference": "34e4824f5fa26aa8e90f1258859c75570c12d27a"
"reference": "05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/stevenmaguire/oauth2-keycloak/zipball/34e4824f5fa26aa8e90f1258859c75570c12d27a",
"reference": "34e4824f5fa26aa8e90f1258859c75570c12d27a",
"url": "https://api.github.com/repos/stevenmaguire/oauth2-keycloak/zipball/05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d",
"reference": "05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d",
"shasum": ""
},
"require": {
"firebase/php-jwt": "~4.0|~5.0",
"league/oauth2-client": "^2.0"
"firebase/php-jwt": "^4.0 || ^5.0 || ^6.0",
"league/oauth2-client": "^2.0",
"php": "~7.2 || ~8.0"
},
"require-dev": {
"mockery/mockery": "~0.9",
"phpunit/phpunit": "~4.0",
"squizlabs/php_codesniffer": "~2.0"
"mockery/mockery": "~1.5.0",
"phpunit/phpunit": "~9.6.4",
"squizlabs/php_codesniffer": "~3.7.0"
},
"type": "library",
"extra": {
@ -1757,9 +1764,9 @@
],
"support": {
"issues": "https://github.com/stevenmaguire/oauth2-keycloak/issues",
"source": "https://github.com/stevenmaguire/oauth2-keycloak/tree/3.2.0"
"source": "https://github.com/stevenmaguire/oauth2-keycloak/tree/4.0.0"
},
"time": "2022-12-16T12:46:38+00:00"
"time": "2023-03-14T09:43:47+00:00"
},
{
"name": "symfony/deprecation-contracts",

View File

@ -217,29 +217,35 @@
},
{
"name": "firebase/php-jwt",
"version": "v5.5.1",
"version_normalized": "5.5.1.0",
"version": "v6.5.0",
"version_normalized": "6.5.0.0",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "83b609028194aa042ea33b5af2d41a7427de80e6"
"reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/83b609028194aa042ea33b5af2d41a7427de80e6",
"reference": "83b609028194aa042ea33b5af2d41a7427de80e6",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/e94e7353302b0c11ec3cfff7180cd0b1743975d2",
"reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
"php": "^7.4||^8.0"
},
"require-dev": {
"phpunit/phpunit": ">=4.8 <=9"
"guzzlehttp/guzzle": "^6.5||^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^1.0||^2.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"time": "2021-11-08T20:18:51+00:00",
"time": "2023-05-12T15:47:07+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -271,7 +277,7 @@
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v5.5.1"
"source": "https://github.com/firebase/php-jwt/tree/v6.5.0"
},
"install-path": "../firebase/php-jwt"
},
@ -1759,29 +1765,30 @@
},
{
"name": "stevenmaguire/oauth2-keycloak",
"version": "3.2.0",
"version_normalized": "3.2.0.0",
"version": "4.0.0",
"version_normalized": "4.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/stevenmaguire/oauth2-keycloak.git",
"reference": "34e4824f5fa26aa8e90f1258859c75570c12d27a"
"reference": "05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/stevenmaguire/oauth2-keycloak/zipball/34e4824f5fa26aa8e90f1258859c75570c12d27a",
"reference": "34e4824f5fa26aa8e90f1258859c75570c12d27a",
"url": "https://api.github.com/repos/stevenmaguire/oauth2-keycloak/zipball/05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d",
"reference": "05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d",
"shasum": ""
},
"require": {
"firebase/php-jwt": "~4.0|~5.0",
"league/oauth2-client": "^2.0"
"firebase/php-jwt": "^4.0 || ^5.0 || ^6.0",
"league/oauth2-client": "^2.0",
"php": "~7.2 || ~8.0"
},
"require-dev": {
"mockery/mockery": "~0.9",
"phpunit/phpunit": "~4.0",
"squizlabs/php_codesniffer": "~2.0"
"mockery/mockery": "~1.5.0",
"phpunit/phpunit": "~9.6.4",
"squizlabs/php_codesniffer": "~3.7.0"
},
"time": "2022-12-16T12:46:38+00:00",
"time": "2023-03-14T09:43:47+00:00",
"type": "library",
"extra": {
"branch-alias": {
@ -1816,7 +1823,7 @@
],
"support": {
"issues": "https://github.com/stevenmaguire/oauth2-keycloak/issues",
"source": "https://github.com/stevenmaguire/oauth2-keycloak/tree/3.2.0"
"source": "https://github.com/stevenmaguire/oauth2-keycloak/tree/4.0.0"
},
"install-path": "../stevenmaguire/oauth2-keycloak"
},

View File

@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '07edec4ea50b8eedae10c28eba0b4b2774df537e',
'reference' => '34e7b3f613661fe2b3c3eb1d316a63dfe111022e',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -13,7 +13,7 @@
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '07edec4ea50b8eedae10c28eba0b4b2774df537e',
'reference' => '34e7b3f613661fe2b3c3eb1d316a63dfe111022e',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -53,9 +53,9 @@
),
),
'firebase/php-jwt' => array(
'pretty_version' => 'v5.5.1',
'version' => '5.5.1.0',
'reference' => '83b609028194aa042ea33b5af2d41a7427de80e6',
'pretty_version' => 'v6.5.0',
'version' => '6.5.0.0',
'reference' => 'e94e7353302b0c11ec3cfff7180cd0b1743975d2',
'type' => 'library',
'install_path' => __DIR__ . '/../firebase/php-jwt',
'aliases' => array(),
@ -275,9 +275,9 @@
'dev_requirement' => false,
),
'stevenmaguire/oauth2-keycloak' => array(
'pretty_version' => '3.2.0',
'version' => '3.2.0.0',
'reference' => '34e4824f5fa26aa8e90f1258859c75570c12d27a',
'pretty_version' => '4.0.0',
'version' => '4.0.0.0',
'reference' => '05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d',
'type' => 'library',
'install_path' => __DIR__ . '/../stevenmaguire/oauth2-keycloak',
'aliases' => array(),

View File

@ -0,0 +1,117 @@
# Changelog
## [6.5.0](https://github.com/firebase/php-jwt/compare/v6.4.0...v6.5.0) (2023-05-12)
### Bug Fixes
* allow KID of '0' ([#505](https://github.com/firebase/php-jwt/issues/505)) ([9dc46a9](https://github.com/firebase/php-jwt/commit/9dc46a9c3e5801294249cfd2554c5363c9f9326a))
### Miscellaneous Chores
* drop support for PHP 7.3 ([#495](https://github.com/firebase/php-jwt/issues/495))
## [6.4.0](https://github.com/firebase/php-jwt/compare/v6.3.2...v6.4.0) (2023-02-08)
### Features
* add support for W3C ES256K ([#462](https://github.com/firebase/php-jwt/issues/462)) ([213924f](https://github.com/firebase/php-jwt/commit/213924f51936291fbbca99158b11bd4ae56c2c95))
* improve caching by only decoding jwks when necessary ([#486](https://github.com/firebase/php-jwt/issues/486)) ([78d3ed1](https://github.com/firebase/php-jwt/commit/78d3ed1073553f7d0bbffa6c2010009a0d483d5c))
## [6.3.2](https://github.com/firebase/php-jwt/compare/v6.3.1...v6.3.2) (2022-11-01)
### Bug Fixes
* check kid before using as array index ([bad1b04](https://github.com/firebase/php-jwt/commit/bad1b040d0c736bbf86814c6b5ae614f517cf7bd))
## [6.3.1](https://github.com/firebase/php-jwt/compare/v6.3.0...v6.3.1) (2022-11-01)
### Bug Fixes
* casing of GET for PSR compat ([#451](https://github.com/firebase/php-jwt/issues/451)) ([60b52b7](https://github.com/firebase/php-jwt/commit/60b52b71978790eafcf3b95cfbd83db0439e8d22))
* string interpolation format for php 8.2 ([#446](https://github.com/firebase/php-jwt/issues/446)) ([2e07d8a](https://github.com/firebase/php-jwt/commit/2e07d8a1524d12b69b110ad649f17461d068b8f2))
## 6.3.0 / 2022-07-15
- Added ES256 support to JWK parsing ([#399](https://github.com/firebase/php-jwt/pull/399))
- Fixed potential caching error in `CachedKeySet` by caching jwks as strings ([#435](https://github.com/firebase/php-jwt/pull/435))
## 6.2.0 / 2022-05-14
- Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397))
- Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)).
## 6.1.0 / 2022-03-23
- Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0
- Add parameter typing and return types where possible
## 6.0.0 / 2022-01-24
- **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information.
- New Key object to prevent key/algorithm type confusion (#365)
- Add JWK support (#273)
- Add ES256 support (#256)
- Add ES384 support (#324)
- Add Ed25519 support (#343)
## 5.0.0 / 2017-06-26
- Support RS384 and RS512.
See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)!
- Add an example for RS256 openssl.
See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)!
- Detect invalid Base64 encoding in signature.
See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)!
- Update `JWT::verify` to handle OpenSSL errors.
See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)!
- Add `array` type hinting to `decode` method
See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)!
- Add all JSON error types.
See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)!
- Bugfix 'kid' not in given key list.
See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)!
- Miscellaneous cleanup, documentation and test fixes.
See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115),
[#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and
[#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman),
[@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)!
## 4.0.0 / 2016-07-17
- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)!
- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)!
- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)!
- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)!
## 3.0.0 / 2015-07-22
- Minimum PHP version updated from `5.2.0` to `5.3.0`.
- Add `\Firebase\JWT` namespace. See
[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to
[@Dashron](https://github.com/Dashron)!
- Require a non-empty key to decode and verify a JWT. See
[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to
[@sjones608](https://github.com/sjones608)!
- Cleaner documentation blocks in the code. See
[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to
[@johanderuijter](https://github.com/johanderuijter)!
## 2.2.0 / 2015-06-22
- Add support for adding custom, optional JWT headers to `JWT::encode()`. See
[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to
[@mcocaro](https://github.com/mcocaro)!
## 2.1.0 / 2015-05-20
- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew
between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)!
- Add support for passing an object implementing the `ArrayAccess` interface for
`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)!
## 2.0.0 / 2015-04-01
- **Note**: It is strongly recommended that you update to > v2.0.0 to address
known security vulnerabilities in prior versions when both symmetric and
asymmetric keys are used together.
- Update signature for `JWT::decode(...)` to require an array of supported
algorithms to use when verifying token signatures.

View File

@ -1,4 +1,4 @@
[![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt)
![Build Status](https://github.com/firebase/php-jwt/actions/workflows/tests.yml/badge.svg)
[![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt)
[![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt)
[![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt)
@ -29,13 +29,13 @@ Example
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$key = "example_key";
$payload = array(
"iss" => "http://example.org",
"aud" => "http://example.com",
"iat" => 1356999524,
"nbf" => 1357000000
);
$key = 'example_key';
$payload = [
'iss' => 'http://example.org',
'aud' => 'http://example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
/**
* IMPORTANT:
@ -65,6 +65,40 @@ $decoded_array = (array) $decoded;
JWT::$leeway = 60; // $leeway in seconds
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
```
Example encode/decode headers
-------
Decoding the JWT headers without verifying the JWT first is NOT recommended, and is not supported by
this library. This is because without verifying the JWT, the header values could have been tampered with.
Any value pulled from an unverified header should be treated as if it could be any string sent in from an
attacker. If this is something you still want to do in your application for whatever reason, it's possible to
decode the header values manually simply by calling `json_decode` and `base64_decode` on the JWT
header part:
```php
use Firebase\JWT\JWT;
$key = 'example_key';
$payload = [
'iss' => 'http://example.org',
'aud' => 'http://example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$headers = [
'x-forwarded-for' => 'www.google.com'
];
// Encode headers in the JWT string
$jwt = JWT::encode($payload, $key, 'HS256', null, $headers);
// Decode headers from the JWT string WITHOUT validation
// **IMPORTANT**: This operation is vulnerable to attacks, as the JWT has not yet been verified.
// These headers could be any value sent by an attacker.
list($headersB64, $payloadB64, $sig) = explode('.', $jwt);
$decoded = json_decode(base64_decode($headersB64), true);
print_r($decoded);
```
Example with RS256 (openssl)
----------------------------
```php
@ -73,37 +107,52 @@ use Firebase\JWT\Key;
$privateKey = <<<EOD
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC8kGa1pSjbSYZVebtTRBLxBz5H4i2p/llLCrEeQhta5kaQu/Rn
vuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t0tyazyZ8JXw+KgXTxldMPEL9
5+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4ehde/zUxo6UvS7UrBQIDAQAB
AoGAb/MXV46XxCFRxNuB8LyAtmLDgi/xRnTAlMHjSACddwkyKem8//8eZtw9fzxz
bWZ/1/doQOuHBGYZU8aDzzj59FZ78dyzNFoF91hbvZKkg+6wGyd/LrGVEB+Xre0J
Nil0GReM2AHDNZUYRv+HYJPIOrB0CRczLQsgFJ8K6aAD6F0CQQDzbpjYdx10qgK1
cP59UHiHjPZYC0loEsk7s+hUmT3QHerAQJMZWC11Qrn2N+ybwwNblDKv+s5qgMQ5
5tNoQ9IfAkEAxkyffU6ythpg/H0Ixe1I2rd0GbF05biIzO/i77Det3n4YsJVlDck
ZkcvY3SK2iRIL4c9yY6hlIhs+K9wXTtGWwJBAO9Dskl48mO7woPR9uD22jDpNSwe
k90OMepTjzSvlhjbfuPN1IdhqvSJTDychRwn1kIJ7LQZgQ8fVz9OCFZ/6qMCQGOb
qaGwHmUK6xzpUbbacnYrIM6nLSkXgOAwv7XXCojvY614ILTK3iXiLBOxPu5Eu13k
eUz9sHyD6vkgZzjtxXECQAkp4Xerf5TGfQXGXhxIX52yH+N2LtujCdkQZjXAsGdm
B2zNzvrlgRmgBrklMTrMYgm1NPcW+bRLGcwgW2PTvNM=
MIIEowIBAAKCAQEAuzWHNM5f+amCjQztc5QTfJfzCC5J4nuW+L/aOxZ4f8J3Frew
M2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJhzkPYLae7bTVro3hok0zDITR8F6S
JGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548tu4czCuqU8BGVOlnp6IqBHhAswNMM
78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vSopcT51koWOgiTf3C7nJUoMWZHZI5
HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTzTTqo1SCSH2pooJl9O8at6kkRYsrZ
WwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/BwQIDAQABAoIBAFtGaOqNKGwggn9k
6yzr6GhZ6Wt2rh1Xpq8XUz514UBhPxD7dFRLpbzCrLVpzY80LbmVGJ9+1pJozyWc
VKeCeUdNwbqkr240Oe7GTFmGjDoxU+5/HX/SJYPpC8JZ9oqgEA87iz+WQX9hVoP2
oF6EB4ckDvXmk8FMwVZW2l2/kd5mrEVbDaXKxhvUDf52iVD+sGIlTif7mBgR99/b
c3qiCnxCMmfYUnT2eh7Vv2LhCR/G9S6C3R4lA71rEyiU3KgsGfg0d82/XWXbegJW
h3QbWNtQLxTuIvLq5aAryV3PfaHlPgdgK0ft6ocU2de2FagFka3nfVEyC7IUsNTK
bq6nhAECgYEA7d/0DPOIaItl/8BWKyCuAHMss47j0wlGbBSHdJIiS55akMvnAG0M
39y22Qqfzh1at9kBFeYeFIIU82ZLF3xOcE3z6pJZ4Dyvx4BYdXH77odo9uVK9s1l
3T3BlMcqd1hvZLMS7dviyH79jZo4CXSHiKzc7pQ2YfK5eKxKqONeXuECgYEAyXlG
vonaus/YTb1IBei9HwaccnQ/1HRn6MvfDjb7JJDIBhNClGPt6xRlzBbSZ73c2QEC
6Fu9h36K/HZ2qcLd2bXiNyhIV7b6tVKk+0Psoj0dL9EbhsD1OsmE1nTPyAc9XZbb
OPYxy+dpBCUA8/1U9+uiFoCa7mIbWcSQ+39gHuECgYAz82pQfct30aH4JiBrkNqP
nJfRq05UY70uk5k1u0ikLTRoVS/hJu/d4E1Kv4hBMqYCavFSwAwnvHUo51lVCr/y
xQOVYlsgnwBg2MX4+GjmIkqpSVCC8D7j/73MaWb746OIYZervQ8dbKahi2HbpsiG
8AHcVSA/agxZr38qvWV54QKBgCD5TlDE8x18AuTGQ9FjxAAd7uD0kbXNz2vUYg9L
hFL5tyL3aAAtUrUUw4xhd9IuysRhW/53dU+FsG2dXdJu6CxHjlyEpUJl2iZu/j15
YnMzGWHIEX8+eWRDsw/+Ujtko/B7TinGcWPz3cYl4EAOiCeDUyXnqnO1btCEUU44
DJ1BAoGBAJuPD27ErTSVtId90+M4zFPNibFP50KprVdc8CR37BE7r8vuGgNYXmnI
RLnGP9p3pVgFCktORuYS2J/6t84I3+A17nEoB4xvhTLeAinAW/uTQOUmNicOP4Ek
2MsLL2kHgL8bLTmvXV4FX+PXphrDKg1XxzOYn0otuoqdAQrkK4og
-----END RSA PRIVATE KEY-----
EOD;
$publicKey = <<<EOD
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8kGa1pSjbSYZVebtTRBLxBz5H
4i2p/llLCrEeQhta5kaQu/RnvuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t
0tyazyZ8JXw+KgXTxldMPEL95+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4
ehde/zUxo6UvS7UrBQIDAQAB
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzWHNM5f+amCjQztc5QT
fJfzCC5J4nuW+L/aOxZ4f8J3FrewM2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJ
hzkPYLae7bTVro3hok0zDITR8F6SJGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548t
u4czCuqU8BGVOlnp6IqBHhAswNMM78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vS
opcT51koWOgiTf3C7nJUoMWZHZI5HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTz
TTqo1SCSH2pooJl9O8at6kkRYsrZWwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/B
wQIDAQAB
-----END PUBLIC KEY-----
EOD;
$payload = array(
"iss" => "example.org",
"aud" => "example.com",
"iat" => 1356999524,
"nbf" => 1357000000
);
$payload = [
'iss' => 'example.org',
'aud' => 'example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$jwt = JWT::encode($payload, $privateKey, 'RS256');
echo "Encode:\n" . print_r($jwt, true) . "\n";
@ -139,12 +188,12 @@ $privateKey = openssl_pkey_get_private(
$passphrase
);
$payload = array(
"iss" => "example.org",
"aud" => "example.com",
"iat" => 1356999524,
"nbf" => 1357000000
);
$payload = [
'iss' => 'example.org',
'aud' => 'example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$jwt = JWT::encode($payload, $privateKey, 'RS256');
echo "Encode:\n" . print_r($jwt, true) . "\n";
@ -173,12 +222,12 @@ $privateKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
$publicKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
$payload = array(
"iss" => "example.org",
"aud" => "example.com",
"iat" => 1356999524,
"nbf" => 1357000000
);
$payload = [
'iss' => 'example.org',
'aud' => 'example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$jwt = JWT::encode($payload, $privateKey, 'EdDSA');
echo "Encode:\n" . print_r($jwt, true) . "\n";
@ -187,6 +236,44 @@ $decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA'));
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
````
Example with multiple keys
--------------------------
```php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Example RSA keys from previous example
// $privateKey1 = '...';
// $publicKey1 = '...';
// Example EdDSA keys from previous example
// $privateKey2 = '...';
// $publicKey2 = '...';
$payload = [
'iss' => 'example.org',
'aud' => 'example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$jwt1 = JWT::encode($payload, $privateKey1, 'RS256', 'kid1');
$jwt2 = JWT::encode($payload, $privateKey2, 'EdDSA', 'kid2');
echo "Encode 1:\n" . print_r($jwt1, true) . "\n";
echo "Encode 2:\n" . print_r($jwt2, true) . "\n";
$keys = [
'kid1' => new Key($publicKey1, 'RS256'),
'kid2' => new Key($publicKey2, 'EdDSA'),
];
$decoded1 = JWT::decode($jwt1, $keys);
$decoded2 = JWT::decode($jwt2, $keys);
echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n";
echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n";
```
Using JWKs
----------
@ -198,72 +285,115 @@ use Firebase\JWT\JWT;
// this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk
$jwks = ['keys' => []];
// JWK::parseKeySet($jwks) returns an associative array of **kid** to private
// key. Pass this as the second parameter to JWT::decode.
// NOTE: The deprecated $supportedAlgorithm must be supplied when parsing from JWK.
JWT::decode($payload, JWK::parseKeySet($jwks), $supportedAlgorithm);
// JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key
// objects. Pass this as the second parameter to JWT::decode.
JWT::decode($payload, JWK::parseKeySet($jwks));
```
Changelog
---------
Using Cached Key Sets
---------------------
#### 5.0.0 / 2017-06-26
- Support RS384 and RS512.
See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)!
- Add an example for RS256 openssl.
See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)!
- Detect invalid Base64 encoding in signature.
See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)!
- Update `JWT::verify` to handle OpenSSL errors.
See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)!
- Add `array` type hinting to `decode` method
See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)!
- Add all JSON error types.
See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)!
- Bugfix 'kid' not in given key list.
See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)!
- Miscellaneous cleanup, documentation and test fixes.
See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115),
[#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and
[#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman),
[@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)!
The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI.
This has the following advantages:
#### 4.0.0 / 2016-07-17
- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)!
- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)!
- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)!
- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)!
1. The results are cached for performance.
2. If an unrecognized key is requested, the cache is refreshed, to accomodate for key rotation.
3. If rate limiting is enabled, the JWKS URI will not make more than 10 requests a second.
#### 3.0.0 / 2015-07-22
- Minimum PHP version updated from `5.2.0` to `5.3.0`.
- Add `\Firebase\JWT` namespace. See
[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to
[@Dashron](https://github.com/Dashron)!
- Require a non-empty key to decode and verify a JWT. See
[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to
[@sjones608](https://github.com/sjones608)!
- Cleaner documentation blocks in the code. See
[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to
[@johanderuijter](https://github.com/johanderuijter)!
```php
use Firebase\JWT\CachedKeySet;
use Firebase\JWT\JWT;
#### 2.2.0 / 2015-06-22
- Add support for adding custom, optional JWT headers to `JWT::encode()`. See
[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to
[@mcocaro](https://github.com/mcocaro)!
// The URI for the JWKS you wish to cache the results from
$jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk';
#### 2.1.0 / 2015-05-20
- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew
between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)!
- Add support for passing an object implementing the `ArrayAccess` interface for
`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)!
// Create an HTTP client (can be any PSR-7 compatible HTTP client)
$httpClient = new GuzzleHttp\Client();
#### 2.0.0 / 2015-04-01
- **Note**: It is strongly recommended that you update to > v2.0.0 to address
known security vulnerabilities in prior versions when both symmetric and
asymmetric keys are used together.
- Update signature for `JWT::decode(...)` to require an array of supported
algorithms to use when verifying token signatures.
// Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory)
$httpFactory = new GuzzleHttp\Psr\HttpFactory();
// Create a cache item pool (can be any PSR-6 compatible cache item pool)
$cacheItemPool = Phpfastcache\CacheManager::getInstance('files');
$keySet = new CachedKeySet(
$jwksUri,
$httpClient,
$httpFactory,
$cacheItemPool,
null, // $expiresAfter int seconds to set the JWKS to expire
true // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys
);
$jwt = 'eyJhbGci...'; // Some JWT signed by a key from the $jwkUri above
$decoded = JWT::decode($jwt, $keySet);
```
Miscellaneous
-------------
#### Exception Handling
When a call to `JWT::decode` is invalid, it will throw one of the following exceptions:
```php
use Firebase\JWT\JWT;
use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;
use Firebase\JWT\ExpiredException;
use DomainException;
use InvalidArgumentException;
use UnexpectedValueException;
try {
$decoded = JWT::decode($payload, $keys);
} catch (InvalidArgumentException $e) {
// provided key/key-array is empty or malformed.
} catch (DomainException $e) {
// provided algorithm is unsupported OR
// provided key is invalid OR
// unknown error thrown in openSSL or libsodium OR
// libsodium is required but not available.
} catch (SignatureInvalidException $e) {
// provided JWT signature verification failed.
} catch (BeforeValidException $e) {
// provided JWT is trying to be used before "nbf" claim OR
// provided JWT is trying to be used before "iat" claim.
} catch (ExpiredException $e) {
// provided JWT is trying to be used after "exp" claim.
} catch (UnexpectedValueException $e) {
// provided JWT is malformed OR
// provided JWT is missing an algorithm / using an unsupported algorithm OR
// provided JWT algorithm does not match provided key OR
// provided key ID in key/key-array is empty or invalid.
}
```
All exceptions in the `Firebase\JWT` namespace extend `UnexpectedValueException`, and can be simplified
like this:
```php
try {
$decoded = JWT::decode($payload, $keys);
} catch (LogicException $e) {
// errors having to do with environmental setup or malformed JWT Keys
} catch (UnexpectedValueException $e) {
// errors having to do with JWT signature and claims
}
```
#### Casting to array
The return value of `JWT::decode` is the generic PHP object `stdClass`. If you'd like to handle with arrays
instead, you can do the following:
```php
// return type is stdClass
$decoded = JWT::decode($payload, $keys);
// cast to array
$decoded = json_decode(json_encode($decoded), true);
```
Tests
-----

View File

@ -20,10 +20,11 @@
],
"license": "BSD-3-Clause",
"require": {
"php": ">=5.3.0"
"php": "^7.4||^8.0"
},
"suggest": {
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present",
"ext-sodium": "Support EdDSA (Ed25519) signatures"
},
"autoload": {
"psr-4": {
@ -31,6 +32,11 @@
}
},
"require-dev": {
"phpunit/phpunit": ">=4.8 <=9"
"guzzlehttp/guzzle": "^6.5||^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^1.0||^2.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
}
}

View File

@ -0,0 +1,258 @@
<?php
namespace Firebase\JWT;
use ArrayAccess;
use InvalidArgumentException;
use LogicException;
use OutOfBoundsException;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use RuntimeException;
use UnexpectedValueException;
/**
* @implements ArrayAccess<string, Key>
*/
class CachedKeySet implements ArrayAccess
{
/**
* @var string
*/
private $jwksUri;
/**
* @var ClientInterface
*/
private $httpClient;
/**
* @var RequestFactoryInterface
*/
private $httpFactory;
/**
* @var CacheItemPoolInterface
*/
private $cache;
/**
* @var ?int
*/
private $expiresAfter;
/**
* @var ?CacheItemInterface
*/
private $cacheItem;
/**
* @var array<string, array<mixed>>
*/
private $keySet;
/**
* @var string
*/
private $cacheKey;
/**
* @var string
*/
private $cacheKeyPrefix = 'jwks';
/**
* @var int
*/
private $maxKeyLength = 64;
/**
* @var bool
*/
private $rateLimit;
/**
* @var string
*/
private $rateLimitCacheKey;
/**
* @var int
*/
private $maxCallsPerMinute = 10;
/**
* @var string|null
*/
private $defaultAlg;
public function __construct(
string $jwksUri,
ClientInterface $httpClient,
RequestFactoryInterface $httpFactory,
CacheItemPoolInterface $cache,
int $expiresAfter = null,
bool $rateLimit = false,
string $defaultAlg = null
) {
$this->jwksUri = $jwksUri;
$this->httpClient = $httpClient;
$this->httpFactory = $httpFactory;
$this->cache = $cache;
$this->expiresAfter = $expiresAfter;
$this->rateLimit = $rateLimit;
$this->defaultAlg = $defaultAlg;
$this->setCacheKeys();
}
/**
* @param string $keyId
* @return Key
*/
public function offsetGet($keyId): Key
{
if (!$this->keyIdExists($keyId)) {
throw new OutOfBoundsException('Key ID not found');
}
return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg);
}
/**
* @param string $keyId
* @return bool
*/
public function offsetExists($keyId): bool
{
return $this->keyIdExists($keyId);
}
/**
* @param string $offset
* @param Key $value
*/
public function offsetSet($offset, $value): void
{
throw new LogicException('Method not implemented');
}
/**
* @param string $offset
*/
public function offsetUnset($offset): void
{
throw new LogicException('Method not implemented');
}
/**
* @return array<mixed>
*/
private function formatJwksForCache(string $jwks): array
{
$jwks = json_decode($jwks, true);
if (!isset($jwks['keys'])) {
throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
}
if (empty($jwks['keys'])) {
throw new InvalidArgumentException('JWK Set did not contain any keys');
}
$keys = [];
foreach ($jwks['keys'] as $k => $v) {
$kid = isset($v['kid']) ? $v['kid'] : $k;
$keys[(string) $kid] = $v;
}
return $keys;
}
private function keyIdExists(string $keyId): bool
{
if (null === $this->keySet) {
$item = $this->getCacheItem();
// Try to load keys from cache
if ($item->isHit()) {
// item found! retrieve it
$this->keySet = $item->get();
// If the cached item is a string, the JWKS response was cached (previous behavior).
// Parse this into expected format array<kid, jwk> instead.
if (\is_string($this->keySet)) {
$this->keySet = $this->formatJwksForCache($this->keySet);
}
}
}
if (!isset($this->keySet[$keyId])) {
if ($this->rateLimitExceeded()) {
return false;
}
$request = $this->httpFactory->createRequest('GET', $this->jwksUri);
$jwksResponse = $this->httpClient->sendRequest($request);
$this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
if (!isset($this->keySet[$keyId])) {
return false;
}
$item = $this->getCacheItem();
$item->set($this->keySet);
if ($this->expiresAfter) {
$item->expiresAfter($this->expiresAfter);
}
$this->cache->save($item);
}
return true;
}
private function rateLimitExceeded(): bool
{
if (!$this->rateLimit) {
return false;
}
$cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
if (!$cacheItem->isHit()) {
$cacheItem->expiresAfter(1); // # of calls are cached each minute
}
$callsPerMinute = (int) $cacheItem->get();
if (++$callsPerMinute > $this->maxCallsPerMinute) {
return true;
}
$cacheItem->set($callsPerMinute);
$this->cache->save($cacheItem);
return false;
}
private function getCacheItem(): CacheItemInterface
{
if (\is_null($this->cacheItem)) {
$this->cacheItem = $this->cache->getItem($this->cacheKey);
}
return $this->cacheItem;
}
private function setCacheKeys(): void
{
if (empty($this->jwksUri)) {
throw new RuntimeException('JWKS URI is empty');
}
// ensure we do not have illegal characters
$key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
// add prefix
$key = $this->cacheKeyPrefix . $key;
// Hash keys if they exceed $maxKeyLength of 64
if (\strlen($key) > $this->maxKeyLength) {
$key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
}
$this->cacheKey = $key;
if ($this->rateLimit) {
// add prefix
$rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
// Hash keys if they exceed $maxKeyLength of 64
if (\strlen($rateLimitKey) > $this->maxKeyLength) {
$rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
}
$this->rateLimitCacheKey = $rateLimitKey;
}
}
}

View File

@ -20,12 +20,25 @@ use UnexpectedValueException;
*/
class JWK
{
private const OID = '1.2.840.10045.2.1';
private const ASN1_OBJECT_IDENTIFIER = 0x06;
private const ASN1_SEQUENCE = 0x10; // also defined in JWT
private const ASN1_BIT_STRING = 0x03;
private const EC_CURVES = [
'P-256' => '1.2.840.10045.3.1.7', // Len: 64
'secp256k1' => '1.3.132.0.10', // Len: 64
// 'P-384' => '1.3.132.0.34', // Len: 96 (not yet supported)
// 'P-521' => '1.3.132.0.35', // Len: 132 (not supported)
];
/**
* Parse a set of JWK keys
*
* @param array $jwks The JSON Web Key Set as an associative array
* @param array<mixed> $jwks The JSON Web Key Set as an associative array
* @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
* JSON Web Key Set
*
* @return array An associative array that represents the set of keys
* @return array<string, Key> An associative array of key IDs (kid) to Key objects
*
* @throws InvalidArgumentException Provided JWK Set is empty
* @throws UnexpectedValueException Provided JWK Set was invalid
@ -33,21 +46,22 @@ class JWK
*
* @uses parseKey
*/
public static function parseKeySet(array $jwks)
public static function parseKeySet(array $jwks, string $defaultAlg = null): array
{
$keys = array();
$keys = [];
if (!isset($jwks['keys'])) {
throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
}
if (empty($jwks['keys'])) {
throw new InvalidArgumentException('JWK Set did not contain any keys');
}
foreach ($jwks['keys'] as $k => $v) {
$kid = isset($v['kid']) ? $v['kid'] : $k;
if ($key = self::parseKey($v)) {
$keys[$kid] = $key;
if ($key = self::parseKey($v, $defaultAlg)) {
$keys[(string) $kid] = $key;
}
}
@ -61,9 +75,11 @@ class JWK
/**
* Parse a JWK key
*
* @param array $jwk An individual JWK
* @param array<mixed> $jwk An individual JWK
* @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
* JSON Web Key Set
*
* @return resource|array An associative array that represents the key
* @return Key The key object for the JWK
*
* @throws InvalidArgumentException Provided JWK is empty
* @throws UnexpectedValueException Provided JWK was invalid
@ -71,15 +87,27 @@ class JWK
*
* @uses createPemFromModulusAndExponent
*/
public static function parseKey(array $jwk)
public static function parseKey(array $jwk, string $defaultAlg = null): ?Key
{
if (empty($jwk)) {
throw new InvalidArgumentException('JWK must not be empty');
}
if (!isset($jwk['kty'])) {
throw new UnexpectedValueException('JWK must contain a "kty" parameter');
}
if (!isset($jwk['alg'])) {
if (\is_null($defaultAlg)) {
// The "alg" parameter is optional in a KTY, but an algorithm is required
// for parsing in this library. Use the $defaultAlg parameter when parsing the
// key set in order to prevent this error.
// @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
throw new UnexpectedValueException('JWK must contain an "alg" parameter');
}
$jwk['alg'] = $defaultAlg;
}
switch ($jwk['kty']) {
case 'RSA':
if (!empty($jwk['d'])) {
@ -96,11 +124,72 @@ class JWK
'OpenSSL error: ' . \openssl_error_string()
);
}
return $publicKey;
return new Key($publicKey, $jwk['alg']);
case 'EC':
if (isset($jwk['d'])) {
// The key is actually a private key
throw new UnexpectedValueException('Key data must be for a public key');
}
if (empty($jwk['crv'])) {
throw new UnexpectedValueException('crv not set');
}
if (!isset(self::EC_CURVES[$jwk['crv']])) {
throw new DomainException('Unrecognised or unsupported EC curve');
}
if (empty($jwk['x']) || empty($jwk['y'])) {
throw new UnexpectedValueException('x and y not set');
}
$publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']);
return new Key($publicKey, $jwk['alg']);
default:
// Currently only RSA is supported
break;
}
return null;
}
/**
* Converts the EC JWK values to pem format.
*
* @param string $crv The EC curve (only P-256 is supported)
* @param string $x The EC x-coordinate
* @param string $y The EC y-coordinate
*
* @return string
*/
private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string
{
$pem =
self::encodeDER(
self::ASN1_SEQUENCE,
self::encodeDER(
self::ASN1_SEQUENCE,
self::encodeDER(
self::ASN1_OBJECT_IDENTIFIER,
self::encodeOID(self::OID)
)
. self::encodeDER(
self::ASN1_OBJECT_IDENTIFIER,
self::encodeOID(self::EC_CURVES[$crv])
)
) .
self::encodeDER(
self::ASN1_BIT_STRING,
\chr(0x00) . \chr(0x04)
. JWT::urlsafeB64Decode($x)
. JWT::urlsafeB64Decode($y)
)
);
return sprintf(
"-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n",
wordwrap(base64_encode($pem), 64, "\n", true)
);
}
/**
@ -113,22 +202,22 @@ class JWK
*
* @uses encodeLength
*/
private static function createPemFromModulusAndExponent($n, $e)
{
$modulus = JWT::urlsafeB64Decode($n);
$publicExponent = JWT::urlsafeB64Decode($e);
private static function createPemFromModulusAndExponent(
string $n,
string $e
): string {
$mod = JWT::urlsafeB64Decode($n);
$exp = JWT::urlsafeB64Decode($e);
$components = array(
'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus),
'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent)
);
$modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
$publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
$rsaPublicKey = \pack(
'Ca*a*a*',
48,
self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])),
$components['modulus'],
$components['publicExponent']
self::encodeLength(\strlen($modulus) + \strlen($publicExponent)),
$modulus,
$publicExponent
);
// sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
@ -143,11 +232,9 @@ class JWK
$rsaOID . $rsaPublicKey
);
$rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" .
return "-----BEGIN PUBLIC KEY-----\r\n" .
\chunk_split(\base64_encode($rsaPublicKey), 64) .
'-----END PUBLIC KEY-----';
return $rsaPublicKey;
}
/**
@ -159,7 +246,7 @@ class JWK
* @param int $length
* @return string
*/
private static function encodeLength($length)
private static function encodeLength(int $length): string
{
if ($length <= 0x7F) {
return \chr($length);
@ -169,4 +256,68 @@ class JWK
return \pack('Ca*', 0x80 | \strlen($temp), $temp);
}
/**
* Encodes a value into a DER object.
* Also defined in Firebase\JWT\JWT
*
* @param int $type DER tag
* @param string $value the value to encode
* @return string the encoded object
*/
private static function encodeDER(int $type, string $value): string
{
$tag_header = 0;
if ($type === self::ASN1_SEQUENCE) {
$tag_header |= 0x20;
}
// Type
$der = \chr($tag_header | $type);
// Length
$der .= \chr(\strlen($value));
return $der . $value;
}
/**
* Encodes a string into a DER-encoded OID.
*
* @param string $oid the OID string
* @return string the binary DER-encoded OID
*/
private static function encodeOID(string $oid): string
{
$octets = explode('.', $oid);
// Get the first octet
$first = (int) array_shift($octets);
$second = (int) array_shift($octets);
$oid = \chr($first * 40 + $second);
// Iterate over subsequent octets
foreach ($octets as $octet) {
if ($octet == 0) {
$oid .= \chr(0x00);
continue;
}
$bin = '';
while ($octet) {
$bin .= \chr(0x80 | ($octet & 0x7f));
$octet >>= 7;
}
$bin[0] = $bin[0] & \chr(0x7f);
// Convert to big endian if necessary
if (pack('V', 65534) == pack('L', 65534)) {
$oid .= strrev($bin);
} else {
$oid .= $bin;
}
}
return $oid;
}
}

View File

@ -3,12 +3,14 @@
namespace Firebase\JWT;
use ArrayAccess;
use DateTime;
use DomainException;
use Exception;
use InvalidArgumentException;
use OpenSSLAsymmetricKey;
use OpenSSLCertificate;
use stdClass;
use UnexpectedValueException;
use DateTime;
/**
* JSON Web Token implementation, based on this spec:
@ -25,52 +27,62 @@ use DateTime;
*/
class JWT
{
const ASN1_INTEGER = 0x02;
const ASN1_SEQUENCE = 0x10;
const ASN1_BIT_STRING = 0x03;
private const ASN1_INTEGER = 0x02;
private const ASN1_SEQUENCE = 0x10;
private const ASN1_BIT_STRING = 0x03;
/**
* When checking nbf, iat or expiration times,
* we want to provide some extra leeway time to
* account for clock skew.
*
* @var int
*/
public static $leeway = 0;
/**
* Allow the current timestamp to be specified.
* Useful for fixing a value within unit testing.
*
* Will default to PHP time() value if null.
*
* @var ?int
*/
public static $timestamp = null;
public static $supported_algs = array(
'ES384' => array('openssl', 'SHA384'),
'ES256' => array('openssl', 'SHA256'),
'HS256' => array('hash_hmac', 'SHA256'),
'HS384' => array('hash_hmac', 'SHA384'),
'HS512' => array('hash_hmac', 'SHA512'),
'RS256' => array('openssl', 'SHA256'),
'RS384' => array('openssl', 'SHA384'),
'RS512' => array('openssl', 'SHA512'),
'EdDSA' => array('sodium_crypto', 'EdDSA'),
);
/**
* @var array<string, string[]>
*/
public static $supported_algs = [
'ES384' => ['openssl', 'SHA384'],
'ES256' => ['openssl', 'SHA256'],
'ES256K' => ['openssl', 'SHA256'],
'HS256' => ['hash_hmac', 'SHA256'],
'HS384' => ['hash_hmac', 'SHA384'],
'HS512' => ['hash_hmac', 'SHA512'],
'RS256' => ['openssl', 'SHA256'],
'RS384' => ['openssl', 'SHA384'],
'RS512' => ['openssl', 'SHA512'],
'EdDSA' => ['sodium_crypto', 'EdDSA'],
];
/**
* Decodes a JWT string into a PHP object.
*
* @param string $jwt The JWT
* @param Key|array<Key>|mixed $keyOrKeyArray The Key or array of Key objects.
* If the algorithm used is asymmetric, this is the public key
* Each Key object contains an algorithm and matching key.
* Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
* 'HS512', 'RS256', 'RS384', and 'RS512'
* @param array $allowed_algs [DEPRECATED] List of supported verification algorithms. Only
* should be used for backwards compatibility.
* @param Key|ArrayAccess<string,Key>|array<string,Key> $keyOrKeyArray The Key or associative array of key IDs
* (kid) to Key objects.
* If the algorithm used is asymmetric, this is
* the public key.
* Each Key object contains an algorithm and
* matching key.
* Supported algorithms are 'ES384','ES256',
* 'HS256', 'HS384', 'HS512', 'RS256', 'RS384'
* and 'RS512'.
*
* @return object The JWT's payload as a PHP object
* @return stdClass The JWT's payload as a PHP object
*
* @throws InvalidArgumentException Provided JWT was empty
* @throws InvalidArgumentException Provided key/key-array was empty or malformed
* @throws DomainException Provided JWT is malformed
* @throws UnexpectedValueException Provided JWT was invalid
* @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed
* @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'
@ -80,27 +92,37 @@ class JWT
* @uses jsonDecode
* @uses urlsafeB64Decode
*/
public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array())
{
public static function decode(
string $jwt,
$keyOrKeyArray
): stdClass {
// Validate JWT
$timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
if (empty($keyOrKeyArray)) {
throw new InvalidArgumentException('Key may not be empty');
}
$tks = \explode('.', $jwt);
if (\count($tks) != 3) {
if (\count($tks) !== 3) {
throw new UnexpectedValueException('Wrong number of segments');
}
list($headb64, $bodyb64, $cryptob64) = $tks;
if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
$headerRaw = static::urlsafeB64Decode($headb64);
if (null === ($header = static::jsonDecode($headerRaw))) {
throw new UnexpectedValueException('Invalid header encoding');
}
if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
$payloadRaw = static::urlsafeB64Decode($bodyb64);
if (null === ($payload = static::jsonDecode($payloadRaw))) {
throw new UnexpectedValueException('Invalid claims encoding');
}
if (false === ($sig = static::urlsafeB64Decode($cryptob64))) {
throw new UnexpectedValueException('Invalid signature encoding');
if (\is_array($payload)) {
// prevent PHP Fatal Error in edge-cases when payload is empty array
$payload = (object) $payload;
}
if (!$payload instanceof stdClass) {
throw new UnexpectedValueException('Payload must be a JSON object');
}
$sig = static::urlsafeB64Decode($cryptob64);
if (empty($header->alg)) {
throw new UnexpectedValueException('Empty algorithm');
}
@ -108,31 +130,18 @@ class JWT
throw new UnexpectedValueException('Algorithm not supported');
}
list($keyMaterial, $algorithm) = self::getKeyMaterialAndAlgorithm(
$keyOrKeyArray,
empty($header->kid) ? null : $header->kid
);
$key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null);
if (empty($algorithm)) {
// Use deprecated "allowed_algs" to determine if the algorithm is supported.
// This opens up the possibility of an attack in some implementations.
// @see https://github.com/firebase/php-jwt/issues/351
if (!\in_array($header->alg, $allowed_algs)) {
throw new UnexpectedValueException('Algorithm not allowed');
}
} else {
// Check the algorithm
if (!self::constantTimeEquals($algorithm, $header->alg)) {
if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) {
// See issue #351
throw new UnexpectedValueException('Incorrect key for this algorithm');
}
}
if ($header->alg === 'ES256' || $header->alg === 'ES384') {
// OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) {
// OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures
$sig = self::signatureToDER($sig);
}
if (!static::verify("$headb64.$bodyb64", $sig, $keyMaterial, $header->alg)) {
if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) {
throw new SignatureInvalidException('Signature verification failed');
}
@ -162,34 +171,37 @@ class JWT
}
/**
* Converts and signs a PHP object or array into a JWT string.
* Converts and signs a PHP array into a JWT string.
*
* @param object|array $payload PHP object or array
* @param string|resource $key The secret key.
* If the algorithm used is asymmetric, this is the private key
* @param string $alg The signing algorithm.
* Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
* 'HS512', 'RS256', 'RS384', and 'RS512'
* @param mixed $keyId
* @param array $head An array with header elements to attach
* @param array<mixed> $payload PHP array
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256',
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
* @param string $keyId
* @param array<string, string> $head An array with header elements to attach
*
* @return string A signed JWT
*
* @uses jsonEncode
* @uses urlsafeB64Encode
*/
public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null)
{
$header = array('typ' => 'JWT', 'alg' => $alg);
public static function encode(
array $payload,
$key,
string $alg,
string $keyId = null,
array $head = null
): string {
$header = ['typ' => 'JWT', 'alg' => $alg];
if ($keyId !== null) {
$header['kid'] = $keyId;
}
if (isset($head) && \is_array($head)) {
$header = \array_merge($head, $header);
}
$segments = array();
$segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
$segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
$segments = [];
$segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header));
$segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload));
$signing_input = \implode('.', $segments);
$signature = static::sign($signing_input, $key, $alg);
@ -202,49 +214,62 @@ class JWT
* Sign a string with a given key and algorithm.
*
* @param string $msg The message to sign
* @param string|resource $key The secret key
* @param string $alg The signing algorithm.
* Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
* 'HS512', 'RS256', 'RS384', and 'RS512'
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256',
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
*
* @return string An encrypted message
*
* @throws DomainException Unsupported algorithm or bad key was specified
*/
public static function sign($msg, $key, $alg = 'HS256')
{
public static function sign(
string $msg,
$key,
string $alg
): string {
if (empty(static::$supported_algs[$alg])) {
throw new DomainException('Algorithm not supported');
}
list($function, $algorithm) = static::$supported_algs[$alg];
switch ($function) {
case 'hash_hmac':
if (!\is_string($key)) {
throw new InvalidArgumentException('key must be a string when using hmac');
}
return \hash_hmac($algorithm, $msg, $key, true);
case 'openssl':
$signature = '';
$success = \openssl_sign($msg, $signature, $key, $algorithm);
$success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line
if (!$success) {
throw new DomainException("OpenSSL unable to sign data");
throw new DomainException('OpenSSL unable to sign data');
}
if ($alg === 'ES256') {
if ($alg === 'ES256' || $alg === 'ES256K') {
$signature = self::signatureFromDER($signature, 256);
} elseif ($alg === 'ES384') {
$signature = self::signatureFromDER($signature, 384);
}
return $signature;
case 'sodium_crypto':
if (!function_exists('sodium_crypto_sign_detached')) {
if (!\function_exists('sodium_crypto_sign_detached')) {
throw new DomainException('libsodium is not available');
}
if (!\is_string($key)) {
throw new InvalidArgumentException('key must be a string when using EdDSA');
}
try {
// The last non-empty line is used as the key.
$lines = array_filter(explode("\n", $key));
$key = base64_decode(end($lines));
$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) {
throw new DomainException($e->getMessage(), 0, $e);
}
}
throw new DomainException('Algorithm not supported');
}
/**
@ -253,15 +278,19 @@ class JWT
*
* @param string $msg The original message (header and body)
* @param string $signature The original signature
* @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
* @param string $alg The algorithm
*
* @return bool
*
* @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure
*/
private static function verify($msg, $signature, $key, $alg)
{
private static function verify(
string $msg,
string $signature,
$keyMaterial,
string $alg
): bool {
if (empty(static::$supported_algs[$alg])) {
throw new DomainException('Algorithm not supported');
}
@ -269,10 +298,11 @@ class JWT
list($function, $algorithm) = static::$supported_algs[$alg];
switch ($function) {
case 'openssl':
$success = \openssl_verify($msg, $signature, $key, $algorithm);
$success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line
if ($success === 1) {
return true;
} elseif ($success === 0) {
}
if ($success === 0) {
return false;
}
// returns 1 on success, 0 on failure, -1 on error.
@ -280,21 +310,33 @@ class JWT
'OpenSSL error: ' . \openssl_error_string()
);
case 'sodium_crypto':
if (!function_exists('sodium_crypto_sign_verify_detached')) {
if (!\function_exists('sodium_crypto_sign_verify_detached')) {
throw new DomainException('libsodium is not available');
}
if (!\is_string($keyMaterial)) {
throw new InvalidArgumentException('key must be a string when using EdDSA');
}
try {
// The last non-empty line is used as the key.
$lines = array_filter(explode("\n", $key));
$key = base64_decode(end($lines));
$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) {
throw new DomainException('Signature cannot be empty string');
}
return sodium_crypto_sign_verify_detached($signature, $msg, $key);
} catch (Exception $e) {
throw new DomainException($e->getMessage(), 0, $e);
}
case 'hash_hmac':
default:
$hash = \hash_hmac($algorithm, $msg, $key, true);
return self::constantTimeEquals($signature, $hash);
if (!\is_string($keyMaterial)) {
throw new InvalidArgumentException('key must be a string when using hmac');
}
$hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
return self::constantTimeEquals($hash, $signature);
}
}
@ -303,30 +345,16 @@ class JWT
*
* @param string $input JSON string
*
* @return object Object representation of JSON string
* @return mixed The decoded JSON string
*
* @throws DomainException Provided string was invalid JSON
*/
public static function jsonDecode($input)
public static function jsonDecode(string $input)
{
if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
/** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you
* to specify that large ints (like Steam Transaction IDs) should be treated as
* strings, rather than the PHP default behaviour of converting them to floats.
*/
$obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
} else {
/** Not all servers will support that, however, so for older versions we must
* manually detect large ints in the JSON string and quote them (thus converting
*them to strings) before decoding, hence the preg_replace() call.
*/
$max_int_length = \strlen((string) PHP_INT_MAX) - 1;
$json_without_bigints = \preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input);
$obj = \json_decode($json_without_bigints);
}
if ($errno = \json_last_error()) {
static::handleJsonError($errno);
self::handleJsonError($errno);
} elseif ($obj === null && $input !== 'null') {
throw new DomainException('Null result with non-null input');
}
@ -334,22 +362,30 @@ class JWT
}
/**
* Encode a PHP object into a JSON string.
* Encode a PHP array into a JSON string.
*
* @param object|array $input A PHP object or array
* @param array<mixed> $input A PHP array
*
* @return string JSON representation of the PHP object or array
* @return string JSON representation of the PHP array
*
* @throws DomainException Provided object could not be encoded to valid JSON
*/
public static function jsonEncode($input)
public static function jsonEncode(array $input): string
{
if (PHP_VERSION_ID >= 50400) {
$json = \json_encode($input, \JSON_UNESCAPED_SLASHES);
} else {
// PHP 5.3 only
$json = \json_encode($input);
}
if ($errno = \json_last_error()) {
static::handleJsonError($errno);
} elseif ($json === 'null' && $input !== null) {
self::handleJsonError($errno);
} elseif ($json === 'null') {
throw new DomainException('Null result with non-null input');
}
if ($json === false) {
throw new DomainException('Provided object could not be encoded to valid JSON');
}
return $json;
}
@ -359,8 +395,10 @@ class JWT
* @param string $input A Base64 encoded string
*
* @return string A decoded string
*
* @throws InvalidArgumentException invalid base64 characters
*/
public static function urlsafeB64Decode($input)
public static function urlsafeB64Decode(string $input): string
{
$remainder = \strlen($input) % 4;
if ($remainder) {
@ -377,7 +415,7 @@ class JWT
*
* @return string The base64 encode of what you passed in
*/
public static function urlsafeB64Encode($input)
public static function urlsafeB64Encode(string $input): string
{
return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
}
@ -386,67 +424,54 @@ class JWT
/**
* Determine if an algorithm has been provided for each Key
*
* @param Key|array<Key>|mixed $keyOrKeyArray
* @param Key|ArrayAccess<string,Key>|array<string,Key> $keyOrKeyArray
* @param string|null $kid
*
* @throws UnexpectedValueException
*
* @return array containing the keyMaterial and algorithm
* @return Key
*/
private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null)
{
if (
is_string($keyOrKeyArray)
|| is_resource($keyOrKeyArray)
|| $keyOrKeyArray instanceof OpenSSLAsymmetricKey
) {
return array($keyOrKeyArray, null);
}
private static function getKey(
$keyOrKeyArray,
?string $kid
): Key {
if ($keyOrKeyArray instanceof Key) {
return array($keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm());
return $keyOrKeyArray;
}
if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) {
if (!isset($kid)) {
if (empty($kid) && $kid !== '0') {
throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
}
if ($keyOrKeyArray instanceof CachedKeySet) {
// Skip "isset" check, as this will automatically refresh if not set
return $keyOrKeyArray[$kid];
}
if (!isset($keyOrKeyArray[$kid])) {
throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
}
$key = $keyOrKeyArray[$kid];
if ($key instanceof Key) {
return array($key->getKeyMaterial(), $key->getAlgorithm());
}
return array($key, null);
}
throw new UnexpectedValueException(
'$keyOrKeyArray must be a string|resource key, an array of string|resource keys, '
. 'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys'
);
return $keyOrKeyArray[$kid];
}
/**
* @param string $left
* @param string $right
* @param string $left The string of known length to compare against
* @param string $right The user-supplied string
* @return bool
*/
public static function constantTimeEquals($left, $right)
public static function constantTimeEquals(string $left, string $right): bool
{
if (\function_exists('hash_equals')) {
return \hash_equals($left, $right);
}
$len = \min(static::safeStrlen($left), static::safeStrlen($right));
$len = \min(self::safeStrlen($left), self::safeStrlen($right));
$status = 0;
for ($i = 0; $i < $len; $i++) {
$status |= (\ord($left[$i]) ^ \ord($right[$i]));
}
$status |= (static::safeStrlen($left) ^ static::safeStrlen($right));
$status |= (self::safeStrlen($left) ^ self::safeStrlen($right));
return ($status === 0);
}
@ -456,17 +481,19 @@ class JWT
*
* @param int $errno An error number from json_last_error()
*
* @throws DomainException
*
* @return void
*/
private static function handleJsonError($errno)
private static function handleJsonError(int $errno): void
{
$messages = array(
$messages = [
JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
);
];
throw new DomainException(
isset($messages[$errno])
? $messages[$errno]
@ -481,7 +508,7 @@ class JWT
*
* @return int
*/
private static function safeStrlen($str)
private static function safeStrlen(string $str): int
{
if (\function_exists('mb_strlen')) {
return \mb_strlen($str, '8bit');
@ -495,10 +522,11 @@ class JWT
* @param string $sig The ECDSA signature to convert
* @return string The encoded DER object
*/
private static function signatureToDER($sig)
private static function signatureToDER(string $sig): string
{
// Separate the signature into r-value and s-value
list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2));
$length = max(1, (int) (\strlen($sig) / 2));
list($r, $s) = \str_split($sig, $length);
// Trim leading zeros
$r = \ltrim($r, "\x00");
@ -525,9 +553,10 @@ class JWT
*
* @param int $type DER tag
* @param string $value the value to encode
*
* @return string the encoded object
*/
private static function encodeDER($type, $value)
private static function encodeDER(int $type, string $value): string
{
$tag_header = 0;
if ($type === self::ASN1_SEQUENCE) {
@ -548,9 +577,10 @@ class JWT
*
* @param string $der binary signature in DER format
* @param int $keySize the number of bits in the key
*
* @return string the signature
*/
private static function signatureFromDER($der, $keySize)
private static function signatureFromDER(string $der, int $keySize): string
{
// OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
list($offset, $_) = self::readDER($der);
@ -575,9 +605,10 @@ class JWT
* @param string $der the binary data in DER format
* @param int $offset the offset of the data stream containing the object
* to decode
* @return array [$offset, $data] the new offset and the decoded object
*
* @return array{int, string|null} the new offset and the decoded object
*/
private static function readDER($der, $offset = 0)
private static function readDER(string $der, int $offset = 0): array
{
$pos = $offset;
$size = \strlen($der);
@ -595,7 +626,7 @@ class JWT
}
// Value
if ($type == self::ASN1_BIT_STRING) {
if ($type === self::ASN1_BIT_STRING) {
$pos++; // Skip the first contents octet (padding indicator)
$data = \substr($der, $pos, $len - 1);
$pos += $len - 1;
@ -606,6 +637,6 @@ class JWT
$data = null;
}
return array($pos, $data);
return [$pos, $data];
}
}

View File

@ -4,37 +4,42 @@ namespace Firebase\JWT;
use InvalidArgumentException;
use OpenSSLAsymmetricKey;
use OpenSSLCertificate;
use TypeError;
class Key
{
/** @var string $algorithm */
/** @var string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */
private $keyMaterial;
/** @var string */
private $algorithm;
/** @var string|resource|OpenSSLAsymmetricKey $keyMaterial */
private $keyMaterial;
/**
* @param string|resource|OpenSSLAsymmetricKey $keyMaterial
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial
* @param string $algorithm
*/
public function __construct($keyMaterial, $algorithm)
{
if (
!is_string($keyMaterial)
&& !is_resource($keyMaterial)
&& !$keyMaterial instanceof OpenSSLAsymmetricKey
public function __construct(
$keyMaterial,
string $algorithm
) {
throw new InvalidArgumentException('Type error: $keyMaterial must be a string, resource, or OpenSSLAsymmetricKey');
if (
!\is_string($keyMaterial)
&& !$keyMaterial instanceof OpenSSLAsymmetricKey
&& !$keyMaterial instanceof OpenSSLCertificate
&& !\is_resource($keyMaterial)
) {
throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey');
}
if (empty($keyMaterial)) {
throw new InvalidArgumentException('Type error: $keyMaterial must not be empty');
throw new InvalidArgumentException('Key material must not be empty');
}
if (!is_string($algorithm)|| empty($keyMaterial)) {
throw new InvalidArgumentException('Type error: $algorithm must be a string');
if (empty($algorithm)) {
throw new InvalidArgumentException('Algorithm must not be empty');
}
// TODO: Remove in PHP 8.0 in favor of class constructor property promotion
$this->keyMaterial = $keyMaterial;
$this->algorithm = $algorithm;
}
@ -44,13 +49,13 @@ class Key
*
* @return string
*/
public function getAlgorithm()
public function getAlgorithm(): string
{
return $this->algorithm;
}
/**
* @return string|resource|OpenSSLAsymmetricKey
* @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate
*/
public function getKeyMaterial()
{

View File

@ -3,9 +3,12 @@ language: php
sudo: false
php:
- 5.6
- 7.0
- 7.1
- 7.2
- 7.3
- 7.4
- 8.0
- 8.1
- 8.2
matrix:
include:

View File

@ -36,6 +36,7 @@ $provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
'encryptionAlgorithm' => 'RS256', // optional
'encryptionKeyPath' => '../key.pem' // optional
'encryptionKey' => 'contents_of_key_or_certificate' // optional
'version' => '20.0.1', // optional
]);
if (!isset($_GET['code'])) {

View File

@ -18,13 +18,14 @@
"keycloak"
],
"require": {
"php": "~7.2 || ~8.0",
"league/oauth2-client": "^2.0",
"firebase/php-jwt": "~4.0|~5.0"
"firebase/php-jwt": "^4.0 || ^5.0 || ^6.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0",
"mockery/mockery": "~0.9",
"squizlabs/php_codesniffer": "~2.0"
"phpunit/phpunit": "~9.6.4",
"mockery/mockery": "~1.5.0",
"squizlabs/php_codesniffer": "~3.7.0"
},
"autoload": {
"psr-4": {
@ -40,5 +41,11 @@
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"scripts": {
"test": [
"@putenv XDEBUG_MODE=coverage",
"phpunit --colors=always"
]
}
}

View File

@ -1,38 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
failOnRisky="true"
failOnWarning="true"
>
<logging>
<log type="coverage-html"
target="./build/coverage/html"
charset="UTF-8"
highlight="false"
<coverage includeUncoveredFiles="true"
pathCoverage="false"
ignoreDeprecatedCodeUnits="true"
disableCodeCoverageIgnore="true">
<include>
<directory suffix=".php">src</directory>
</include>
<exclude>
<directory suffix=".php">vendor</directory>
<file>src/autoload.php</file>
</exclude>
<report>
<html outputDirectory="./build/coverage/html"
lowUpperBound="35"
highLowerBound="70"/>
<log type="coverage-clover"
target="./build/coverage/log/coverage.xml"/>
</logging>
<clover outputFile="./build/coverage/log/coverage.xml"/>
</report>
</coverage>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./test/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./</directory>
<exclude>
<directory suffix=".php">./examples</directory>
<directory suffix=".php">./vendor</directory>
<directory suffix=".php">./test</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@ -23,18 +23,22 @@ namespace Stevenmaguire\OAuth2\Client\Provider
namespace Stevenmaguire\OAuth2\Client\Test\Provider
{
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Tool\QueryBuilderTrait;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use Stevenmaguire\OAuth2\Client\Provider\Exception\EncryptionConfigurationException;
use Stevenmaguire\OAuth2\Client\Provider\Keycloak;
class KeycloakTest extends \PHPUnit_Framework_TestCase
class KeycloakTest extends TestCase
{
use QueryBuilderTrait;
protected $provider;
protected function setUp()
protected function setUp(): void
{
$this->provider = new \Stevenmaguire\OAuth2\Client\Provider\Keycloak([
$this->provider = new Keycloak([
'authServerUrl' => 'http://mock.url/auth',
'realm' => 'mock_realm',
'clientId' => 'mock_client_id',
@ -43,7 +47,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
]);
}
public function tearDown()
public function tearDown(): void
{
m::close();
parent::tearDown();
@ -67,7 +71,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
public function testEncryptionAlgorithm()
{
$algorithm = uniqid();
$provider = new \Stevenmaguire\OAuth2\Client\Provider\Keycloak([
$provider = new Keycloak([
'encryptionAlgorithm' => $algorithm,
]);
@ -82,7 +86,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
public function testEncryptionKey()
{
$key = uniqid();
$provider = new \Stevenmaguire\OAuth2\Client\Provider\Keycloak([
$provider = new Keycloak([
'encryptionKey' => $key,
]);
@ -101,7 +105,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$key = uniqid();
$mockFileGetContents = $key;
$provider = new \Stevenmaguire\OAuth2\Client\Provider\Keycloak([
$provider = new Keycloak([
'encryptionKeyPath' => $path,
]);
@ -118,12 +122,14 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
public function testEncryptionKeyPathFails()
{
$this->markTestIncomplete('Need to assess the test to see what is required to be checked.');
global $mockFileGetContents;
$path = uniqid();
$key = uniqid();
$mockFileGetContents = new \Exception();
$provider = new \Stevenmaguire\OAuth2\Client\Provider\Keycloak([
$provider = new Keycloak([
'encryptionKeyPath' => $path,
]);
@ -137,7 +143,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$query = ['scope' => implode($scopeSeparator, $options['scope'])];
$url = $this->provider->getAuthorizationUrl($options);
$encodedScope = $this->buildQueryString($query);
$this->assertContains($encodedScope, $url);
$this->assertStringContainsString($encodedScope, $url);
}
public function testGetAuthorizationUrl()
@ -169,11 +175,15 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
public function testGetAccessToken()
{
$response = m::mock('Psr\Http\Message\ResponseInterface');
$response->shouldReceive('getBody')->andReturn('{"access_token":"mock_access_token", "scope":"email", "token_type":"bearer"}');
$response->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$response->shouldReceive('getBody')
->andReturn('{"access_token":"mock_access_token", "scope":"email", "token_type":"bearer"}');
$response->shouldReceive('getHeader')
->andReturn(['content-type' => 'json']);
$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')->times(1)->andReturn($response);
$client->shouldReceive('send')
->times(1)
->andReturn($response);
$this->provider->setHttpClient($client);
$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
@ -186,18 +196,24 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
public function testUserData()
{
$userId = rand(1000,9999);
$userId = rand(1000, 9999);
$name = uniqid();
$nickname = uniqid();
$email = uniqid();
$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')->andReturn('access_token=mock_access_token&expires=3600&refresh_token=mock_refresh_token&otherKey={1234}');
$postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'application/x-www-form-urlencoded']);
$postResponse->shouldReceive('getBody')
->andReturn(
'access_token=mock_access_token&expires=3600&refresh_token=mock_refresh_token&otherKey={1234}'
);
$postResponse->shouldReceive('getHeader')
->andReturn(['content-type' => 'application/x-www-form-urlencoded']);
$userResponse = m::mock('Psr\Http\Message\ResponseInterface');
$userResponse->shouldReceive('getBody')->andReturn('{"sub": '.$userId.', "name": "'.$name.'", "email": "'.$email.'"}');
$userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$userResponse->shouldReceive('getBody')
->andReturn('{"sub": '.$userId.', "name": "'.$name.'", "email": "'.$email.'"}');
$userResponse->shouldReceive('getHeader')
->andReturn(['content-type' => 'json']);
$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
@ -218,7 +234,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
public function testUserDataWithEncryption()
{
$userId = rand(1000,9999);
$userId = rand(1000, 9999);
$name = uniqid();
$nickname = uniqid();
$email = uniqid();
@ -227,17 +243,27 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$key = uniqid();
$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')->andReturn('access_token=mock_access_token&expires=3600&refresh_token=mock_refresh_token&otherKey={1234}');
$postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'application/x-www-form-urlencoded']);
$postResponse->shouldReceive('getStatusCode')->andReturn(200);
$postResponse->shouldReceive('getBody')
->andReturn(
'access_token=mock_access_token&expires=3600&refresh_token=mock_refresh_token&otherKey={1234}'
);
$postResponse->shouldReceive('getHeader')
->andReturn(['content-type' => 'application/x-www-form-urlencoded']);
$postResponse->shouldReceive('getStatusCode')
->andReturn(200);
$userResponse = m::mock('Psr\Http\Message\ResponseInterface');
$userResponse->shouldReceive('getBody')->andReturn($jwt);
$userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'application/jwt']);
$userResponse->shouldReceive('getStatusCode')->andReturn(200);
$userResponse->shouldReceive('getBody')
->andReturn($jwt);
$userResponse->shouldReceive('getHeader')
->andReturn(['content-type' => 'application/jwt']);
$userResponse->shouldReceive('getStatusCode')
->andReturn(200);
$decoder = \Mockery::mock('overload:Firebase\JWT\JWT');
$decoder->shouldReceive('decode')->with($jwt, $key, [$algorithm])->andReturn([
$decoder->shouldReceive('decode')
->with($jwt, $key, [$algorithm])
->andReturn([
'sub' => $userId,
'email' => $email,
'name' => $name,
@ -262,20 +288,27 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$this->assertEquals($email, $user->toArray()['email']);
}
/**
* @expectedException Stevenmaguire\OAuth2\Client\Provider\Exception\EncryptionConfigurationException
*/
public function testUserDataFailsWhenEncryptionEncounteredAndNotConfigured()
{
$this->expectException(EncryptionConfigurationException::class);
$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')->andReturn('access_token=mock_access_token&expires=3600&refresh_token=mock_refresh_token&otherKey={1234}');
$postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'application/x-www-form-urlencoded']);
$postResponse->shouldReceive('getStatusCode')->andReturn(200);
$postResponse->shouldReceive('getBody')
->andReturn(
'access_token=mock_access_token&expires=3600&refresh_token=mock_refresh_token&otherKey={1234}'
);
$postResponse->shouldReceive('getHeader')
->andReturn(['content-type' => 'application/x-www-form-urlencoded']);
$postResponse->shouldReceive('getStatusCode')
->andReturn(200);
$userResponse = m::mock('Psr\Http\Message\ResponseInterface');
$userResponse->shouldReceive('getBody')->andReturn(uniqid());
$userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'application/jwt']);
$userResponse->shouldReceive('getStatusCode')->andReturn(200);
$userResponse->shouldReceive('getBody')
->andReturn(uniqid());
$userResponse->shouldReceive('getHeader')
->andReturn(['content-type' => 'application/jwt']);
$userResponse->shouldReceive('getStatusCode')
->andReturn(200);
$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
@ -287,17 +320,20 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$user = $this->provider->getResourceOwner($token);
}
/**
* @expectedException League\OAuth2\Client\Provider\Exception\IdentityProviderException
*/
public function testErrorResponse()
{
$this->expectException(IdentityProviderException::class);
$response = m::mock('Psr\Http\Message\ResponseInterface');
$response->shouldReceive('getBody')->andReturn('{"error": "invalid_grant", "error_description": "Code not found"}');
$response->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$response->shouldReceive('getBody')
->andReturn('{"error": "invalid_grant", "error_description": "Code not found"}');
$response->shouldReceive('getHeader')
->andReturn(['content-type' => 'json']);
$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')->times(1)->andReturn($response);
$client->shouldReceive('send')
->times(1)
->andReturn($response);
$this->provider->setHttpClient($client);
$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);