Files
DomFramework/jwt.php
2019-12-05 10:04:52 +00:00

298 lines
10 KiB
PHP

<?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)
* Allowed algorithms : HS256, HS512, HS384
* @param string|null $ckey The cipher key to encrypt the payload (24 chars
* length)
* @return string The Token
*/
public function encode ($payload, $key, $alg = "HS256", $ckey = null)
// {{{
{
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));
$payload = $this->jsonEncode ($payload);
if ($ckey)
$payload = $this->encrypt ($payload, $ckey);
$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 (24 chars
* length)
* @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)
// {{{
{
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)
$payload = $this->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))
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));
}
// }}}
/** Encrypt the payload to not be readable by anybody
* @param string $payload The payload to encrypt
* @param string $ckey The 24 chars for the cipher key
* @param string|null $cipherMethod DES-EDE3-CBC by default
* @return base64 encrypted payload
*/
private function encrypt ($payload, $ckey, $cipherMethod = "des-ede3-cbc")
// {{{
{
if (! in_array ($cipherMethod, openssl_get_cipher_methods()))
throw new \Exception (dgettext ("domframework",
"Invalid cipher provided to encrypt method : ".
"doesn't exists in OpenSSL"), 500);
if (! is_string ($payload))
throw new \Exception (dgettext ("domframework",
"Invalid payload provided to encrypt method : ".
"Not a string"), 500);
if (strlen ($ckey) !== 24)
throw new \Exception (dgettext ("domframework",
"Invalid cipherKey provided to encrypt method :" .
" length different of 24 chars"), 500);
// Must be the same as decrypt
$options = true;
$ivlen = openssl_cipher_iv_length ($cipherMethod);
$iv = openssl_random_pseudo_bytes ($ivlen);
$ciphertext = openssl_encrypt ($payload, $cipherMethod, $ckey, $options,
$iv);
if ($ciphertext === false)
throw new \Exception (dgettext ("domframework",
"Can not encrypt the payload"), 500);
$ciphertext = $iv . $ciphertext;
return ($ciphertext);
}
// }}}
/** Decrypt the payload
* @param string $ciphertext The payload to decrypt
* @param string $ckey The 24 chars for the cipher key
* @param string|null $cipherMethod DES-EDE3-CBC by default
* @return decrypted payload
*/
private function decrypt ($ciphertext, $ckey, $cipherMethod = "des-ede3-cbc")
// {{{
{
if (! is_string ($ciphertext))
throw new \Exception (dgettext ("domframework",
"Invalid ciphertext provided to decrypt method : not a string"), 500);
if (trim ($ciphertext) === "")
throw new \Exception (dgettext ("domframework",
"Invalid ciphertext provided to decrypt method : empty string"), 500);
if (strlen ($ckey) !== 24)
throw new \Exception (dgettext ("domframework",
"Invalid cipherKey provided to decrypt method :" .
" length different of 24 chars"), 500);
$ivlen = openssl_cipher_iv_length ($cipherMethod);
$iv = substr ($ciphertext, 0, $ivlen);
if (strlen ($iv) != $ivlen)
throw new \Exception (dgettext ("domframework",
"Can not decrypt the payload : invalid salt"), 500);
// Must be the same as encrypt
$options = true;
$ciphertext = substr ($ciphertext, $ivlen);
return openssl_decrypt ($ciphertext, $cipherMethod, $ckey, $options, $iv);
}
// }}}
/** 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);
}
// }}}
/** 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);
}
// }}}
}