Skip to content

Commit

Permalink
Updated JWT lib to address #564
Browse files Browse the repository at this point in the history
JWT::decode() now takes an array of $allowed_algorithms to prevent tricking the server into decoding using a different algorithm than intended.

Addresses #564
  • Loading branch information
phindmarsh authored and bshaffer committed Apr 22, 2015
1 parent 67afff5 commit 2acf2c1
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 38 deletions.
3 changes: 2 additions & 1 deletion src/OAuth2/Encryption/EncryptionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

interface EncryptionInterface
{
public function getSupportedAlgorithms($type = null);
public function encode($payload, $key, $algorithm = null);
public function decode($payload, $key, $algorithm = null);
public function decode($payload, $key, $allowed_algorithms = null);
public function urlSafeB64Encode($data);
public function urlSafeB64Decode($b64);
}
97 changes: 65 additions & 32 deletions src/OAuth2/Encryption/Jwt.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
*/
class Jwt implements EncryptionInterface
{

public static $supportedAlgorithms = array(
'HS256' => array('hash_hmac', 'sha256'),
'HS384' => array('hash_hmac', 'sha384'),
'HS512' => array('hash_hmac', 'sha512'),
'RS256' => array('openssl', 'sha256', 'OPENSSL_ALGO_SHA256'),
'RS384' => array('openssl', 'sha384', 'OPENSSL_ALGO_SHA384'),
'RS512' => array('openssl', 'sha512', 'OPENSSL_ALGO_SHA512')
);

public function encode($payload, $key, $algo = 'HS256')
{
$header = $this->generateJwtHeader($payload, $algo);
Expand All @@ -25,7 +35,7 @@ public function encode($payload, $key, $algo = 'HS256')
return implode('.', $segments);
}

public function decode($jwt, $key = null, $verify = true)
public function decode($jwt, $key = null, $allowed_algorithms = null)
{
if (!strpos($jwt, '.')) {
return false;
Expand All @@ -49,11 +59,25 @@ public function decode($jwt, $key = null, $verify = true)

$sig = $this->urlSafeB64Decode($cryptob64);

if ($verify) {
if (isset($key)) {
if (!isset($header['alg'])) {
return false;
}

<<<<<<< Updated upstream
if (!is_array($allowed_algorithms) || !in_array($header['alg'], $allowed_algorithms)){
return false;
}

=======
<<<<<<< Updated upstream
=======
if (!in_array($header['alg'], (array) $allowed_algorithms)) {
return false;
}

>>>>>>> Stashed changes
>>>>>>> Stashed changes
if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header['alg'])) {
return false;
}
Expand All @@ -64,24 +88,20 @@ public function decode($jwt, $key = null, $verify = true)

private function verifySignature($signature, $input, $key, $algo = 'HS256')
{
// use constants when possible, for HipHop support
switch ($algo) {
case'HS256':
case'HS384':
case'HS512':
list($function, $algorithm) = self::$supportedAlgorithms[$algo];
switch ($function) {
case 'hash_hmac':
return $this->hash_equals(
$this->sign($input, $key, $algo),
$signature
);

case 'RS256':
return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256') === 1;

case 'RS384':
return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384') === 1;

case 'RS512':
return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512') === 1;
case 'openssl':
// use constants when possible, for HipHop support
if(defined(self::$supportedAlgorithms[$algo][2])){
$algorithm = constant(self::$supportedAlgorithms[$algo][2]);
}
return @openssl_verify($input, $signature, $key, $algorithm) === 1;

default:
throw new \InvalidArgumentException("Unsupported or invalid signing algorithm.");
Expand All @@ -90,24 +110,17 @@ private function verifySignature($signature, $input, $key, $algo = 'HS256')

private function sign($input, $key, $algo = 'HS256')
{
switch ($algo) {
case 'HS256':
return hash_hmac('sha256', $input, $key, true);

case 'HS384':
return hash_hmac('sha384', $input, $key, true);
list($function, $algorithm) = self::$supportedAlgorithms[$algo];

case 'HS512':
return hash_hmac('sha512', $input, $key, true);
switch ($function) {
case 'hash_hmac':
return hash_hmac($algorithm, $input, $key, true);

case 'RS256':
return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256');

case 'RS384':
return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384');

case 'RS512':
return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512');
case 'openssl':
if(defined(self::$supportedAlgorithms[$algo][2])){
$algorithm = constant(self::$supportedAlgorithms[$algo][2]);
}
return $this->generateRSASignature($input, $key, $algorithm);

default:
throw new \Exception("Unsupported or invalid signing algorithm.");
Expand Down Expand Up @@ -142,6 +155,25 @@ public function urlSafeB64Decode($b64)
return base64_decode($b64);
}

public function getSupportedAlgorithms($type = null)
{
if ($type === null) {
return array_keys(self::$supportedAlgorithms);
}
else {
$filtered = array();
foreach (self::$supportedAlgorithms as $alg => $method) {
if ($type === 'RSA' && $alg[0] === 'R') {
$filtered[] = $alg;
}
else if ($type === 'HMAC' && $alg[0] === 'H') {
$filtered[] = $alg;
}
}
return $filtered;
}
}

/**
* Override to create a custom header
*/
Expand All @@ -152,7 +184,7 @@ protected function generateJwtHeader($payload, $algorithm)
'alg' => $algorithm,
);
}

protected function hash_equals($a, $b)
{
if (function_exists('hash_equals')) {
Expand All @@ -164,4 +196,5 @@ protected function hash_equals($a, $b)
}
return $diff === 0;
}

}
6 changes: 4 additions & 2 deletions src/OAuth2/GrantType/JwtBearer.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re
$undecodedJWT = $request->request('assertion');

// Decode the JWT
$jwt = $this->jwtUtil->decode($request->request('assertion'), null, false);
$jwt = $this->jwtUtil->decode($request->request('assertion'), null);

if (!$jwt) {
$response->setError(400, 'invalid_request', "JWT is malformed");
Expand Down Expand Up @@ -176,8 +176,10 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re
return null;
}

// get the supported RSA algorithms from the jwtUtil
$allowed_algorithms = $this->jwtUtil->getSupportedAlgorithms('RSA');
// Verify the JWT
if (!$this->jwtUtil->decode($undecodedJWT, $key, true)) {
if (!$this->jwtUtil->decode($undecodedJWT, $key, $allowed_algorithms)) {
$response->setError(400, 'invalid_grant', "JWT failed signature verification");

return null;
Expand Down
4 changes: 2 additions & 2 deletions src/OAuth2/Storage/JwtAccessToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function __construct(PublicKeyInterface $publicKeyStorage, AccessTokenInt
public function getAccessToken($oauth_token)
{
// just decode the token, don't verify
if (!$tokenData = $this->encryptionUtil->decode($oauth_token, null, false)) {
if (!$tokenData = $this->encryptionUtil->decode($oauth_token, null)) {
return false;
}

Expand All @@ -44,7 +44,7 @@ public function getAccessToken($oauth_token)
$algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id);

// now that we have the client_id, verify the token
if (false === $this->encryptionUtil->decode($oauth_token, $public_key, true)) {
if (false === $this->encryptionUtil->decode($oauth_token, $public_key, array($algorithm))) {
return false;
}

Expand Down
25 changes: 24 additions & 1 deletion test/OAuth2/Encryption/JwtTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function testJwtUtil($client_id, $client_key)

$encoded = $jwtUtil->encode($params, $this->privateKey, 'RS256');

$payload = $jwtUtil->decode($encoded, $client_key);
$payload = $jwtUtil->decode($encoded, $client_key, array('RS256'));

$this->assertEquals($params, $payload);
}
Expand All @@ -58,6 +58,29 @@ public function testInvalidJwt()
$this->assertFalse($jwtUtil->decode('go.o.b'));
}

/** @dataProvider provideClientCredentials */
public function testInvalidJwtHeader($client_id, $client_key)
{
$jwtUtil = new Jwt();

$params = array(
'iss' => $client_id,
'exp' => time() + 1000,
'iat' => time(),
'sub' => 'testuser@ourdomain.com',
'aud' => 'http://myapp.com/oauth/auth',
'scope' => null,
);

// testing for algorithm tampering when only RSA256 signing is allowed
// @see https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/
$tampered = $jwtUtil->encode($params, $client_key, 'HS256');

$payload = $jwtUtil->decode($tampered, $client_key, array('RS256'));

$this->assertFalse($payload);
}

public function provideClientCredentials()
{
$storage = Bootstrap::getInstance()->getMemoryStorage();
Expand Down

0 comments on commit 2acf2c1

Please sign in to comment.