Skip to content

Commit

Permalink
feat: require "Key" object when decoding JWTs
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer committed Nov 3, 2021
1 parent 65920fd commit c6964eb
Show file tree
Hide file tree
Showing 21 changed files with 140 additions and 151 deletions.
10 changes: 9 additions & 1 deletion src/JWK.php
Expand Up @@ -47,7 +47,15 @@ public static function parseKeySet(array $jwks)
foreach ($jwks['keys'] as $k => $v) {
$kid = isset($v['kid']) ? $v['kid'] : $k;
if ($key = self::parseKey($v)) {
$keys[$kid] = $key;
if (isset($v['alg'])) {
$keys[$kid] = new Key($key, $v['alg']);
} else {
// The "alg" parameter is optional in a KTY, but is required
// for parsing in this library. Add it manually to y our JWK
// array if it doesn't already exist.
// @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
throw new InvalidArgumentException('JWK key is missing "alg"');
}
}
}

Expand Down
60 changes: 22 additions & 38 deletions src/JWT.php
Expand Up @@ -5,8 +5,6 @@
use ArrayAccess;
use DomainException;
use Exception;
use Firebase\JWT\Keys\JWTKey;
use Firebase\JWT\Keys\Keyring;
use InvalidArgumentException;
use UnexpectedValueException;
use DateTime;
Expand Down Expand Up @@ -81,8 +79,9 @@ class JWT
* @uses jsonDecode
* @uses urlsafeB64Decode
*/
public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array())
public static function decode($jwt, $keyOrKeyArray)
{
// Validate JWT
$timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;

if (empty($keyOrKeyArray)) {
Expand All @@ -109,31 +108,18 @@ public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array(
throw new UnexpectedValueException('Algorithm not supported');
}

list($keyMaterial, $algorithm) = self::getKeyMaterialAndAlgorithm(
$keyOrKeyArray,
empty($header->kid) ? null : $header->kid
);
$key = self::getKey($keyOrKeyArray, empty($header->kid) ? null : $header->kid);

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)) {
// See issue #351
throw new UnexpectedValueException('Incorrect key for this algorithm');
}
// Check the algorithm
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
$sig = self::signatureToDER($sig);
}

if (!static::verify("$headb64.$bodyb64", $sig, $keyMaterial, $header->alg)) {
if (!static::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) {
throw new SignatureInvalidException('Signature verification failed');
}

Expand Down Expand Up @@ -391,36 +377,34 @@ public static function urlsafeB64Encode($input)
*
* @return an array containing the keyMaterial and algorithm
*/
private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null)
private static function getKey($keyOrKeyArray, $kid = null)
{
if (is_string($keyOrKeyArray)) {
return array($keyOrKeyArray, null);
}

if ($keyOrKeyArray instanceof Key) {
return array($keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm());
return $keyOrKeyArray;
}

if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) {
foreach ($keyOrKeyArray as $keyId => $key) {
if (!$key instanceof Key) {
throw new UnexpectedValueException(
'$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an '
. 'array of Firebase\JWT\Key keys'
);
}
}
if (!isset($kid)) {
throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
}
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);
return $keyOrKeyArray[$kid];
}

throw new UnexpectedValueException(
'$keyOrKeyArray must be a string key, an array of string keys, '
. 'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys'
'$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an '
. 'array of Firebase\JWT\Key keys'
);
}

Expand Down Expand Up @@ -475,7 +459,7 @@ private static function handleJsonError($errno)
*
* @return int
*/
public static function safeStrlen($str)
private static function safeStrlen($str)
{
if (\function_exists('mb_strlen')) {
return \mb_strlen($str, '8bit');
Expand Down
2 changes: 1 addition & 1 deletion src/Key.php
Expand Up @@ -49,7 +49,7 @@ public function getAlgorithm()
}

/**
* @return string|resource
* @return string|resource|OpenSSLAsymmetricKey
*/
public function getKeyMaterial()
{
Expand Down
38 changes: 27 additions & 11 deletions tests/JWKTest.php
Expand Up @@ -38,34 +38,50 @@ public function testParsePrivateKey()
'UnexpectedValueException',
'RSA private keys are not supported'
);

$jwkSet = json_decode(
file_get_contents(__DIR__ . '/rsa-jwkset.json'),
file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
true
);
$jwkSet['keys'][0]['d'] = 'privatekeyvalue';


JWK::parseKeySet($jwkSet);
}

public function testParsePrivateKeyWithoutAlg()
{
$this->setExpectedException(
'InvalidArgumentException',
'JWK key is missing "alg"'
);

$jwkSet = json_decode(
file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
true
);
unset($jwkSet['keys'][0]['alg']);

JWK::parseKeySet($jwkSet);
}

public function testParseKeyWithEmptyDValue()
{
$jwkSet = json_decode(
file_get_contents(__DIR__ . '/rsa-jwkset.json'),
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__ . '/rsa-jwkset.json'),
file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
true
);
$keys = JWK::parseKeySet($jwkSet);
Expand Down Expand Up @@ -93,7 +109,7 @@ public function testParseJwkKeySet_empty()
*/
public function testDecodeByJwkKeySetTokenExpired()
{
$privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem');
$privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem');
$payload = array('exp' => strtotime('-1 hour'));
$msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1');

Expand All @@ -107,7 +123,7 @@ public function testDecodeByJwkKeySetTokenExpired()
*/
public function testDecodeByJwkKeySet()
{
$privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem');
$privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem');
$payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds'));
$msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1');

Expand All @@ -121,7 +137,7 @@ public function testDecodeByJwkKeySet()
*/
public function testDecodeByMultiJwkKeySet()
{
$privKey2 = file_get_contents(__DIR__ . '/rsa2-private.pem');
$privKey2 = file_get_contents(__DIR__ . '/data/rsa2-private.pem');
$payload = array('sub' => 'bar', 'exp' => strtotime('+10 seconds'));
$msg = JWT::encode($payload, $privKey2, 'RS256', 'jwk2');

Expand Down

0 comments on commit c6964eb

Please sign in to comment.