From 5dfb2c2d300b8749625a239c51adb16cc0482600 Mon Sep 17 00:00:00 2001 From: Dominique Fournier Date: Fri, 24 May 2019 08:58:14 +0000 Subject: [PATCH] Add JSON Web Token class git-svn-id: https://svn.fournier38.fr/svn/ProgSVN/trunk@5283 bf3deb0d-5f1a-0410-827f-c0cc1f45334c --- Tests/jwtTest.php | 133 ++++++++++++++++++++++++++++ jwt.php | 216 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 Tests/jwtTest.php create mode 100644 jwt.php diff --git a/Tests/jwtTest.php b/Tests/jwtTest.php new file mode 100644 index 0000000..6ac477b --- /dev/null +++ b/Tests/jwtTest.php @@ -0,0 +1,133 @@ + + */ + +/** 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"); + } +} diff --git a/jwt.php b/jwt.php new file mode 100644 index 0000000..97daadc --- /dev/null +++ b/jwt.php @@ -0,0 +1,216 @@ + + */ + +/** 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); + } + // }}} +}