* @license BSD */ namespace Domframework; /** * Allow to manage the JSON Web Tokens * Based on https://tools.ietf.org/html/rfc7519 * * Do not put confidential data in payload without encrypt it, as the result * is only a Base64 format of JSON... */ class Jwt { // PROPERTIES /** * List the allowed algorithms to sign the token */ private $supportedAlgs = [ 'HS256' => ['hash_hmac', 'SHA256'], 'HS512' => ['hash_hmac', 'SHA512'], 'HS384' => ['hash_hmac', 'SHA384'], ]; /** * Create the token based on payload, sign it with key, and optionally * encrypt it with ckey * Do not put confidential data in payload without encrypt it, as the result * is only a Base64 format of JSON... * @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) * Allowed algorithms : HS256, HS512, HS384 * @param string|null $ckey The cipher key to encrypt the payload * @param string|null $cipherMethod The method to cipher the payload * des-ede3-cbc by default * @return string The Token */ public function encode( $payload, $key, $alg = "HS256", $ckey = null, $cipherMethod = "des-ede3-cbc" ) { if (! key_exists($alg, $this->supportedAlgs)) { throw new \Exception(dgettext( "domframework", "Invalid encode algorithm requested : not allowed" ), 500); } $header = ["typ" => "JWT", "alg" => $alg]; $segments = []; $segments[] = $this->urlsafeB64Encode($this->jsonEncode($header)); $payload = $this->jsonEncode($payload); if ($ckey) { $encrypt = new Encrypt(); $payload = $encrypt->encrypt($payload, $ckey, $cipherMethod); } $segments[] = $this->urlsafeB64Encode($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 * @param string|null $ckey The cipher key to decrypt the payload * @param string|null $cipherMethod The method to cipher the payload * des-ede3-cbc by default * @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, $ckey = null, $cipherMethod = "des-ede3-cbc" ) { 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 = (object)$this->jsonDecode($this->urlsafeB64Decode($headerb64)); $payload = $this->urlsafeB64Decode($payloadb64); if ($ckey) { $encrypt = new Encrypt(); $payload = $encrypt->decrypt($payload, $ckey); } $payload = $this->jsonDecode($payload); $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, true)) { 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 */ private 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 */ private 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 * To be URL compliant, the slash and plus are converted to underscore and * dash * @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, true); } /** * 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); } }