Add JSON Web Token class
git-svn-id: https://svn.fournier38.fr/svn/ProgSVN/trunk@5283 bf3deb0d-5f1a-0410-827f-c0cc1f45334c
This commit is contained in:
133
Tests/jwtTest.php
Normal file
133
Tests/jwtTest.php
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
/** DomFramework - Tests
|
||||||
|
* @package domframework
|
||||||
|
* @author Dominique Fournier <dominique@fournier38.fr>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Test the jwt.php file */
|
||||||
|
class test_jwt extends PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
public function test_createKey_1 ()
|
||||||
|
{
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$res = $jwt->createKey ();
|
||||||
|
$this->assertSame (40, strlen ($res));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_sign_1 ()
|
||||||
|
{
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$res = $jwt->sign ("TEXT TO SIGN", "KEY TO USE", "HS384");
|
||||||
|
$this->assertSame (
|
||||||
|
"cQB+yNVvIER+Nw53MZfU/PGPAJlkKUnjMikmXAwVB9tcaINQH5a88LCDi0PmI5mZ",
|
||||||
|
base64_encode ($res));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_sign_2 ()
|
||||||
|
{
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$res = $jwt->sign ("text to sign", "KEY TO USE", "HS384");
|
||||||
|
$this->assertSame (
|
||||||
|
"FLSkslsUGIpkP3xsJx5ephnCtH7K4jZSNxRxxCn3m7fsPK/MMfEIVr+h3heap80x",
|
||||||
|
base64_encode ($res));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_sign_3 ()
|
||||||
|
{
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$res = $jwt->sign ("text to sign", "key to use", "HS384");
|
||||||
|
$this->assertSame (
|
||||||
|
"lBLlXb5Xo3z9zoEuO0obZdhqGNUKr8DaEsL991TpSPWIdB2067ckR+AJ1FW6in2B",
|
||||||
|
base64_encode ($res));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_encode_1 ()
|
||||||
|
{
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$res = $jwt->encode (array ("payload" => "value"), "key to use", "HS384");
|
||||||
|
$this->assertSame (
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.".
|
||||||
|
"eyJwYXlsb2FkIjoidmFsdWUifQ.".
|
||||||
|
"0ByHaODQQjYEvmgU2u5LI034RRMc7CKJQ752ys19Fqj7QiTJO7-trerYKCxCyuge", $res);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_decode_1 ()
|
||||||
|
{
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$res = $jwt->decode (
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.".
|
||||||
|
"eyJwYXlsb2FkIjoidmFsdWUifQ.".
|
||||||
|
"0ByHaODQQjYEvmgU2u5LI034RRMc7CKJQ752ys19Fqj7QiTJO7-trerYKCxCyuge",
|
||||||
|
"key to use");
|
||||||
|
$this->assertSame ((object) (array ("payload" => "value")), $res);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_decode_2 ()
|
||||||
|
{
|
||||||
|
$GLOBALS["hash_equals"] = false;
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$res = $jwt->decode (
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.".
|
||||||
|
"eyJwYXlsb2FkIjoidmFsdWUifQ.".
|
||||||
|
"0ByHaODQQjYEvmgU2u5LI034RRMc7CKJQ752ys19Fqj7QiTJO7-trerYKCxCyuge",
|
||||||
|
"key to use");
|
||||||
|
$this->assertSame ((object) (array ("payload" => "value")), $res);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_decode_3 ()
|
||||||
|
{
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$this->expectException ("Exception", "JWT Header not readable");
|
||||||
|
$res = $jwt->decode (
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUXXXXXJ9.".
|
||||||
|
"eyJwYXlsb2FkIjoidmFsdWUifQ.".
|
||||||
|
"0ByHaODQQjYEvmgU2u5LI034RRMc7CKJQ752ys19Fqj7QiTJO7-trerYKCxCyuge",
|
||||||
|
"key to use");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_decode_4 ()
|
||||||
|
{
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$this->expectException ("Exception", "JWT Payload not readable");
|
||||||
|
$res = $jwt->decode (
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.".
|
||||||
|
"eyJwYXlsb2FkIjoiXXXXXXXXfQ.".
|
||||||
|
"0ByHaODQQjYEvmgU2u5LI034RRMc7CKJQ752ys19Fqj7QiTJO7-trerYKCxCyuge",
|
||||||
|
"key to use");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_decode_5 ()
|
||||||
|
{
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$this->expectException ("Exception",
|
||||||
|
"JWT Signature verification failed");
|
||||||
|
$res = $jwt->decode (
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.".
|
||||||
|
"eyJwYXlsb2FkIjoidmFsdWUifQ.".
|
||||||
|
"1ByHaODQQjYEvmgU2u5LI034RRMc7CKJQ752ys19Fqj7QiTJO7-trerYKCxCyuge",
|
||||||
|
"key to use");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_decode_6 ()
|
||||||
|
{
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$this->expectException ("Exception",
|
||||||
|
"JWT Signature not readable");
|
||||||
|
$res = $jwt->decode (
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.".
|
||||||
|
"eyJwYXlsb2FkIjoidmFsdWUifQ.".
|
||||||
|
"0",
|
||||||
|
"key to use");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_decode_7 ()
|
||||||
|
{
|
||||||
|
$jwt = new jwt ();
|
||||||
|
$this->expectException ("Exception",
|
||||||
|
"Malformed JWT Token");
|
||||||
|
$res = $jwt->decode (
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.".
|
||||||
|
"eyJwYXlsb2FkIjoidmFsdWUifQ",
|
||||||
|
"key to use");
|
||||||
|
}
|
||||||
|
}
|
||||||
216
jwt.php
Normal file
216
jwt.php
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
<?php
|
||||||
|
/** DomFramework
|
||||||
|
* @package domframework
|
||||||
|
* @author Dominique Fournier <dominique@fournier38.fr>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Allow to manage the JSON Web Tokens
|
||||||
|
* Based on https://tools.ietf.org/html/rfc7519
|
||||||
|
*/
|
||||||
|
class jwt
|
||||||
|
{
|
||||||
|
// PROPERTIES
|
||||||
|
// {{{
|
||||||
|
/** List the allowed algorithms to sign the token
|
||||||
|
*/
|
||||||
|
private $supportedAlgs = array (
|
||||||
|
'HS256' => array('hash_hmac', 'SHA256'),
|
||||||
|
'HS512' => array('hash_hmac', 'SHA512'),
|
||||||
|
'HS384' => array('hash_hmac', 'SHA384'),
|
||||||
|
);
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
/** Create the token based on payload, key
|
||||||
|
* @param array $payload The payload to store
|
||||||
|
* @param string $key The key to be used to sign the token
|
||||||
|
* @param string|null $alg The algorithm to use to sign the token (default
|
||||||
|
* is HS256)
|
||||||
|
* @return string The Token
|
||||||
|
*/
|
||||||
|
public function encode ($payload, $key, $alg = "HS256")
|
||||||
|
// {{{
|
||||||
|
{
|
||||||
|
if (! key_exists ($alg, $this->supportedAlgs))
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"Invalid encode algorithm requested : not allowed"), 500);
|
||||||
|
$header = array ("typ" => "JWT", "alg" => $alg);
|
||||||
|
$segments = array ();
|
||||||
|
$segments[] = $this->urlsafeB64Encode ($this->jsonEncode ($header));
|
||||||
|
$segments[] = $this->urlsafeB64Encode ($this->jsonEncode ($payload));
|
||||||
|
$toBeSigned = implode ('.', $segments);
|
||||||
|
$signature = $this->sign ($toBeSigned, $key, $alg);
|
||||||
|
$segments[] = $this->urlsafeB64Encode ($signature);
|
||||||
|
return implode ('.', $segments);
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
/** Decode the provide JWT and return an array of the payload
|
||||||
|
* @param string $jwt The token to examine
|
||||||
|
* @param string $key The key used to sign the message
|
||||||
|
* @param array|null $allowedAlg List of allowed algorithms. If null, all the
|
||||||
|
* algorithms defined in $this->supportedAlgs are allowed
|
||||||
|
* @return array the decoded payload
|
||||||
|
* @throw Exception if the key is not able to verify the token with the
|
||||||
|
* provided password
|
||||||
|
*/
|
||||||
|
public function decode ($jwt, $key, $allowedAlg = null)
|
||||||
|
{
|
||||||
|
if ($allowedAlg === null)
|
||||||
|
$allowedAlg = array_keys ($this->supportedAlgs);
|
||||||
|
if (empty ($key))
|
||||||
|
throw new \Exception (dgettext ("domframework", "Key may not be empty"),
|
||||||
|
500);
|
||||||
|
$tks = explode(".", $jwt);
|
||||||
|
if (count ($tks) != 3)
|
||||||
|
throw new \Exception (dgettext ("domframework", "Malformed JWT Token"),
|
||||||
|
403);
|
||||||
|
list ($headerb64, $payloadb64, $signb64) = $tks;
|
||||||
|
$header = $this->jsonDecode ($this->urlsafeB64Decode ($headerb64));
|
||||||
|
$payload = $this->jsonDecode ($this->urlsafeB64Decode ($payloadb64));
|
||||||
|
$signature = $this->urlsafeB64Decode ($signb64);
|
||||||
|
if ($header === null)
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"JWT Header not readable"), 403);
|
||||||
|
if ($payload === null)
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"JWT Payload not readable"), 403);
|
||||||
|
if ($signature === false)
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"JWT Signature not readable"), 403);
|
||||||
|
if (empty ($header->alg))
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"JWT with Empty algorithm"), 403);
|
||||||
|
if (! in_array ($header->alg, $allowedAlg))
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"JWT with Invalid algorithm"), 403);
|
||||||
|
if (empty ($header->typ))
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"JWT with Empty type set"), 403);
|
||||||
|
if ($header->typ !== "JWT")
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"JWT with Invalid type"), 403);
|
||||||
|
if (! $this->verify("$headerb64.$payloadb64", $signature, $key,
|
||||||
|
$header->alg))
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"JWT Signature verification failed"), 403);
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verify the provided token with the key and generate an return true if it
|
||||||
|
* can be verify
|
||||||
|
* @param string $input The text in Base64 to check
|
||||||
|
* @param string $sign The user provided signature in binary
|
||||||
|
* @param string $key The key to use to sign the input
|
||||||
|
* @param string $alg The algorithm to use to sign the input
|
||||||
|
* @return boolean Return true if the input signed is valid
|
||||||
|
*/
|
||||||
|
public function verify ($input, $sign, $key, $alg)
|
||||||
|
// {{{
|
||||||
|
{
|
||||||
|
$signature = $this->sign ($input, $key, $alg);
|
||||||
|
if (function_exists ("hash_equals") &&
|
||||||
|
(! key_exists ("hash_equals", $GLOBALS) ||
|
||||||
|
$GLOBALS["hash_equals"] === true))
|
||||||
|
return hash_equals ($signature, $sign);
|
||||||
|
if (strlen ($signature) !== strlen ($sign))
|
||||||
|
return false;
|
||||||
|
$status = 0;
|
||||||
|
for ($i = 0; $i < strlen ($signature); $i++)
|
||||||
|
$status |= ord ($signature[$i]) ^ ord ($sign[$i]);
|
||||||
|
return $status === 0;
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
/** Create a signing key
|
||||||
|
* @return string the signing key proposed
|
||||||
|
*/
|
||||||
|
public function createKey ()
|
||||||
|
// {{{
|
||||||
|
{
|
||||||
|
return sha1 (microtime (true));
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
/** Sign the requested string with the provided key and based on the algorithm
|
||||||
|
* @param string $input The string to sign
|
||||||
|
* @param string $key The key to use
|
||||||
|
* @param string $alg The algorithm to use to sign
|
||||||
|
* @return string The signed string in binary
|
||||||
|
*/
|
||||||
|
public function sign ($input, $key, $alg)
|
||||||
|
// {{{
|
||||||
|
{
|
||||||
|
if (! key_exists ($alg, $this->supportedAlgs))
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"Invalid encode algorithm requested : not allowed"), 500);
|
||||||
|
list ($method, $algorithm) = $this->supportedAlgs[$alg];
|
||||||
|
switch($method)
|
||||||
|
{
|
||||||
|
case 'hash_hmac':
|
||||||
|
return hash_hmac ($algorithm, $input, $key, true);
|
||||||
|
default:
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"Invalid method to sign the JWT"), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
/** Return the provided string in base64 without equal at the end
|
||||||
|
* @param string $str The string the convert in base64
|
||||||
|
* @return string The string converted in base64
|
||||||
|
*/
|
||||||
|
private function urlsafeB64Encode ($str)
|
||||||
|
// {{{
|
||||||
|
{
|
||||||
|
return rtrim (strtr (base64_encode ($str), '+/', '-_'), "=");
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
/** Return the provided base64 to string
|
||||||
|
* @param string $str The string the convert from base64
|
||||||
|
* @return string The string converted from base64
|
||||||
|
*/
|
||||||
|
private function urlsafeB64Decode ($str)
|
||||||
|
// {{{
|
||||||
|
{
|
||||||
|
$str = strtr ($str, '-_', '+/');
|
||||||
|
$remainder = strlen ($str) % 4;
|
||||||
|
switch (strlen ($str) % 4)
|
||||||
|
{
|
||||||
|
case 0: break;
|
||||||
|
case 2: $str .= "=="; break;
|
||||||
|
case 3: $str .= "="; break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return base64_decode ($str);
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
/** Return the provided array to JSON string
|
||||||
|
* @param object $input The object to convert in JSON
|
||||||
|
* @return string The JSON string
|
||||||
|
*/
|
||||||
|
private function jsonEncode ($input)
|
||||||
|
// {{{
|
||||||
|
{
|
||||||
|
$json = json_encode ($input);
|
||||||
|
if ($json === "null" && $input !== null)
|
||||||
|
throw new \Exception (dgettext ("domframework",
|
||||||
|
"JSON Encode : Null result with non-null input"), 500);
|
||||||
|
return $json;
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
/** Decode the provided JSON string and return the result
|
||||||
|
* If null, there is a decode problem
|
||||||
|
* @param string $input The string to decode
|
||||||
|
* @return mixed The decoded string in object
|
||||||
|
*/
|
||||||
|
private function jsonDecode ($input)
|
||||||
|
// {{{
|
||||||
|
{
|
||||||
|
return json_decode ($input, false, 512, JSON_BIGINT_AS_STRING);
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user