Skip to content

Commit 28eb0e3

Browse files
committed
add key object support in v5
1 parent e08160a commit 28eb0e3

File tree

8 files changed

+166
-338
lines changed

8 files changed

+166
-338
lines changed

.github/actions/entrypoint.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ apt-get install -y --no-install-recommends \
55
git \
66
zip \
77
curl \
8+
ca-certificates \
89
unzip \
910
wget
1011

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Example
2727
-------
2828
```php
2929
use Firebase\JWT\JWT;
30+
use Firebase\JWT\Key;
3031

3132
$key = "example_key";
3233
$payload = array(
@@ -43,7 +44,7 @@ $payload = array(
4344
* for a list of spec-compliant algorithms.
4445
*/
4546
$jwt = JWT::encode($payload, $key);
46-
$decoded = JWT::decode($jwt, $key, array('HS256'));
47+
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
4748

4849
print_r($decoded);
4950

@@ -62,12 +63,13 @@ $decoded_array = (array) $decoded;
6263
* Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef
6364
*/
6465
JWT::$leeway = 60; // $leeway in seconds
65-
$decoded = JWT::decode($jwt, $key, array('HS256'));
66+
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
6667
```
6768
Example with RS256 (openssl)
6869
----------------------------
6970
```php
7071
use Firebase\JWT\JWT;
72+
use Firebase\JWT\Key;
7173

7274
$privateKey = <<<EOD
7375
-----BEGIN RSA PRIVATE KEY-----
@@ -106,7 +108,7 @@ $payload = array(
106108
$jwt = JWT::encode($payload, $privateKey, 'RS256');
107109
echo "Encode:\n" . print_r($jwt, true) . "\n";
108110

109-
$decoded = JWT::decode($jwt, $publicKey, array('RS256'));
111+
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
110112

111113
/*
112114
NOTE: This will now be an object instead of an associative array. To get
@@ -121,6 +123,9 @@ Example with a passphrase
121123
-------------------------
122124

123125
```php
126+
use Firebase\JWT\JWT;
127+
use Firebase\JWT\Key;
128+
124129
// Your passphrase
125130
$passphrase = '[YOUR_PASSPHRASE]';
126131

@@ -147,14 +152,15 @@ echo "Encode:\n" . print_r($jwt, true) . "\n";
147152
// Get public key from the private key, or pull from from a file.
148153
$publicKey = openssl_pkey_get_details($privateKey)['key'];
149154

150-
$decoded = JWT::decode($jwt, $publicKey, array('RS256'));
155+
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
151156
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
152157
```
153158

154159
Example with EdDSA (libsodium and Ed25519 signature)
155160
----------------------------
156161
```php
157162
use Firebase\JWT\JWT;
163+
use Firebase\JWT\Key;
158164

159165
// Public and private keys are expected to be Base64 encoded. The last
160166
// non-empty line is used so that keys can be generated with
@@ -177,7 +183,7 @@ $payload = array(
177183
$jwt = JWT::encode($payload, $privateKey, 'EdDSA');
178184
echo "Encode:\n" . print_r($jwt, true) . "\n";
179185

180-
$decoded = JWT::decode($jwt, $publicKey, array('EdDSA'));
186+
$decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA'));
181187
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
182188
````
183189

@@ -194,7 +200,7 @@ $jwks = ['keys' => []];
194200

195201
// JWK::parseKeySet($jwks) returns an associative array of **kid** to private
196202
// key. Pass this as the second parameter to JWT::decode.
197-
JWT::decode($payload, JWK::parseKeySet($jwks), $supportedAlgorithm);
203+
JWT::decode($payload, JWK::parseKeySet($jwks));
198204
```
199205

200206
Changelog

src/JWT.php

Lines changed: 67 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,13 @@ class JWT
6161
* Decodes a JWT string into a PHP object.
6262
*
6363
* @param string $jwt The JWT
64-
* @param string|array|resource $key The key, or map of keys.
64+
* @param Key|array<Key> $keyOrKeyArray The Key or array of Key objects.
6565
* If the algorithm used is asymmetric, this is the public key
66-
* @param array $allowed_algs List of supported verification algorithms
66+
* Each Key object contains an algorithm and matching key.
6767
* Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
6868
* 'HS512', 'RS256', 'RS384', and 'RS512'
69+
* @param array $allowed_algs [DEPRECATED] List of supported verification algorithms. Only
70+
* should be used for BC.
6971
*
7072
* @return object The JWT's payload as a PHP object
7173
*
@@ -79,11 +81,11 @@ class JWT
7981
* @uses jsonDecode
8082
* @uses urlsafeB64Decode
8183
*/
82-
public static function decode($jwt, $key, array $allowed_algs = array())
84+
public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array())
8385
{
8486
$timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
8587

86-
if (empty($key)) {
88+
if (empty($keyOrKeyArray)) {
8789
throw new InvalidArgumentException('Key may not be empty');
8890
}
8991
$tks = \explode('.', $jwt);
@@ -106,36 +108,32 @@ public static function decode($jwt, $key, array $allowed_algs = array())
106108
if (empty(static::$supported_algs[$header->alg])) {
107109
throw new UnexpectedValueException('Algorithm not supported');
108110
}
109-
if (!\in_array($header->alg, $allowed_algs)) {
110-
throw new UnexpectedValueException('Algorithm not allowed');
111+
112+
list($keyMaterial, $algorithm) = self::getKeyMaterialAndAlgorithm(
113+
$keyOrKeyArray,
114+
empty($header->kid) ? null : $header->kid
115+
);
116+
117+
if (empty($algorithm)) {
118+
// Use deprecated "allowed_algs" to determine if the algorithm is supported.
119+
// This opens up the possibility of an attack in some implementations.
120+
// @see https://github.com/firebase/php-jwt/issues/351
121+
if (!\in_array($header->alg, $allowed_algs)) {
122+
throw new UnexpectedValueException('Algorithm not allowed');
123+
}
124+
} else {
125+
// Check the algorithm
126+
if (!self::constantTimeEquals($algorithm, $header->alg)) {
127+
// See issue #351
128+
throw new UnexpectedValueException('Incorrect key for this algorithm');
129+
}
111130
}
112131
if ($header->alg === 'ES256' || $header->alg === 'ES384') {
113132
// OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
114133
$sig = self::signatureToDER($sig);
115134
}
116135

117-
/** @var Keyring|JWTKey $key */
118-
$key = self::getKeyType($key, $allowed_algs);
119-
if ($key instanceof Keyring) {
120-
if (isset($header->kid)) {
121-
if (!isset($key[$header->kid])) {
122-
throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
123-
}
124-
$key = $key[$header->kid];
125-
} else {
126-
throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
127-
}
128-
}
129-
if (!($key instanceof JWTKey)) {
130-
throw new UnexpectedValueException('$key should be an instance of JWTKey');
131-
}
132-
133-
// Check the signature
134-
if (!$key->isValidForAlg($header->alg)) {
135-
// See issue #351
136-
throw new UnexpectedValueException('Incorrect key for this algorithm');
137-
}
138-
if (!static::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) {
136+
if (!static::verify("$headb64.$bodyb64", $sig, $keyMaterial, $header->alg)) {
139137
throw new SignatureInvalidException('Signature verification failed');
140138
}
141139

@@ -385,6 +383,47 @@ public static function urlsafeB64Encode($input)
385383
return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
386384
}
387385

386+
387+
/**
388+
* Determine if an algorithm has been provided for each Key
389+
*
390+
* @param string|array $keyOrKeyArray
391+
*
392+
* @return an array containing the keyMaterial and algorithm
393+
*/
394+
private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null)
395+
{
396+
if (is_string($keyOrKeyArray)) {
397+
return [$keyOrKeyArray, null];
398+
}
399+
400+
if ($keyOrKeyArray instanceof Key) {
401+
return [$keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm()];
402+
}
403+
404+
if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) {
405+
if (!isset($kid)) {
406+
throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
407+
}
408+
if (!isset($keyOrKeyArray[$kid])) {
409+
throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
410+
}
411+
412+
$key = $keyOrKeyArray[$kid];
413+
414+
if ($key instanceof Key) {
415+
return [$key->getKeyMaterial(), $key->getAlgorithm()];
416+
}
417+
418+
return [$key, null];
419+
}
420+
421+
throw new UnexpectedValueException(
422+
'$keyOrKeyArray must be a string key, an array of string keys, '
423+
. 'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys'
424+
);
425+
}
426+
388427
/**
389428
* @param string $left
390429
* @param string $right
@@ -406,29 +445,6 @@ public static function constantTimeEquals($left, $right)
406445
return ($status === 0);
407446
}
408447

409-
/**
410-
* @param string|array|ArrayAccess $oldType
411-
* @param string[] $algs
412-
* @return KeyInterface
413-
*/
414-
public static function getKeyType($oldType, $algs)
415-
{
416-
if ($oldType instanceof KeyInterface) {
417-
return $oldType;
418-
}
419-
if (is_string($oldType)) {
420-
return new JWTKey($oldType, $algs);
421-
}
422-
if (is_array($oldType) || $oldType instanceof ArrayAccess) {
423-
$keyring = new Keyring(array());
424-
foreach ($oldType as $kid => $key) {
425-
$keyring[$kid] = new JWTKey($key, $algs);
426-
}
427-
return $keyring;
428-
}
429-
throw new InvalidArgumentException('Invalid type: Must be string or array');
430-
}
431-
432448
/**
433449
* Helper method to create a JSON error.
434450
*

src/Key.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace Firebase\JWT;
4+
5+
use InvalidArgumentException;
6+
use OpenSSLAsymmetricKey;
7+
8+
class Key
9+
{
10+
/** @var string $algorithm */
11+
private $algorithm;
12+
13+
/** @var string $keyMaterial */
14+
private $keyMaterial;
15+
16+
/**
17+
* @param string|resource $keyMaterial
18+
* @param string $algorithm
19+
*/
20+
public function __construct($keyMaterial, $algorithm)
21+
{
22+
if (
23+
!is_string($keyMaterial)
24+
&& !is_resource($keyMaterial)
25+
&& !$keyMaterial instanceof OpenSSLAsymmetricKey
26+
) {
27+
throw new InvalidArgumentException('Type error: $keyMaterial must be a string, resource, or OpenSSLAsymmetricKey');
28+
}
29+
30+
if (empty($keyMaterial)) {
31+
throw new InvalidArgumentException('Type error: $keyMaterial must not be empty');
32+
}
33+
34+
if (!is_string($algorithm)|| empty($keyMaterial)) {
35+
throw new InvalidArgumentException('Type error: $algorithm must be a string');
36+
}
37+
$this->keyMaterial = $keyMaterial;
38+
$this->algorithm = $algorithm;
39+
}
40+
41+
/**
42+
* Return the algorithm valid for this key
43+
*
44+
* @return string
45+
*/
46+
public function getAlgorithm()
47+
{
48+
return $this->algorithm;
49+
}
50+
51+
/**
52+
* @return string|resource
53+
*/
54+
public function getKeyMaterial()
55+
{
56+
return $this->keyMaterial;
57+
}
58+
}

0 commit comments

Comments
 (0)