vendor/firebase/php-jwt/src/JWT.php line 83

Open in your IDE?
  1. <?php
  2. namespace Firebase\JWT;
  3. use ArrayAccess;
  4. use DomainException;
  5. use Exception;
  6. use InvalidArgumentException;
  7. use OpenSSLAsymmetricKey;
  8. use UnexpectedValueException;
  9. use DateTime;
  10. /**
  11.  * JSON Web Token implementation, based on this spec:
  12.  * https://tools.ietf.org/html/rfc7519
  13.  *
  14.  * PHP version 5
  15.  *
  16.  * @category Authentication
  17.  * @package  Authentication_JWT
  18.  * @author   Neuman Vong <neuman@twilio.com>
  19.  * @author   Anant Narayanan <anant@php.net>
  20.  * @license  http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
  21.  * @link     https://github.com/firebase/php-jwt
  22.  */
  23. class JWT
  24. {
  25.     const ASN1_INTEGER 0x02;
  26.     const ASN1_SEQUENCE 0x10;
  27.     const ASN1_BIT_STRING 0x03;
  28.     /**
  29.      * When checking nbf, iat or expiration times,
  30.      * we want to provide some extra leeway time to
  31.      * account for clock skew.
  32.      */
  33.     public static $leeway 0;
  34.     /**
  35.      * Allow the current timestamp to be specified.
  36.      * Useful for fixing a value within unit testing.
  37.      *
  38.      * Will default to PHP time() value if null.
  39.      */
  40.     public static $timestamp null;
  41.     public static $supported_algs = array(
  42.         'ES384' => array('openssl''SHA384'),
  43.         'ES256' => array('openssl''SHA256'),
  44.         'HS256' => array('hash_hmac''SHA256'),
  45.         'HS384' => array('hash_hmac''SHA384'),
  46.         'HS512' => array('hash_hmac''SHA512'),
  47.         'RS256' => array('openssl''SHA256'),
  48.         'RS384' => array('openssl''SHA384'),
  49.         'RS512' => array('openssl''SHA512'),
  50.         'EdDSA' => array('sodium_crypto''EdDSA'),
  51.     );
  52.     /**
  53.      * Decodes a JWT string into a PHP object.
  54.      *
  55.      * @param string                    $jwt            The JWT
  56.      * @param Key|array<Key>|mixed      $keyOrKeyArray  The Key or array of Key objects.
  57.      *                                                  If the algorithm used is asymmetric, this is the public key
  58.      *                                                  Each Key object contains an algorithm and matching key.
  59.      *                                                  Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
  60.      *                                                  'HS512', 'RS256', 'RS384', and 'RS512'
  61.      * @param array                     $allowed_algs   [DEPRECATED] List of supported verification algorithms. Only
  62.      *                                                  should be used for backwards  compatibility.
  63.      *
  64.      * @return object The JWT's payload as a PHP object
  65.      *
  66.      * @throws InvalidArgumentException     Provided JWT was empty
  67.      * @throws UnexpectedValueException     Provided JWT was invalid
  68.      * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
  69.      * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
  70.      * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
  71.      * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
  72.      *
  73.      * @uses jsonDecode
  74.      * @uses urlsafeB64Decode
  75.      */
  76.     public static function decode($jwt$keyOrKeyArray, array $allowed_algs = array())
  77.     {
  78.         $timestamp \is_null(static::$timestamp) ? \time() : static::$timestamp;
  79.         if (empty($keyOrKeyArray)) {
  80.             throw new InvalidArgumentException('Key may not be empty');
  81.         }
  82.         $tks \explode('.'$jwt);
  83.         if (\count($tks) != 3) {
  84.             throw new UnexpectedValueException('Wrong number of segments');
  85.         }
  86.         list($headb64$bodyb64$cryptob64) = $tks;
  87.         if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
  88.             throw new UnexpectedValueException('Invalid header encoding');
  89.         }
  90.         if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
  91.             throw new UnexpectedValueException('Invalid claims encoding');
  92.         }
  93.         if (false === ($sig = static::urlsafeB64Decode($cryptob64))) {
  94.             throw new UnexpectedValueException('Invalid signature encoding');
  95.         }
  96.         if (empty($header->alg)) {
  97.             throw new UnexpectedValueException('Empty algorithm');
  98.         }
  99.         if (empty(static::$supported_algs[$header->alg])) {
  100.             throw new UnexpectedValueException('Algorithm not supported');
  101.         }
  102.         list($keyMaterial$algorithm) = self::getKeyMaterialAndAlgorithm(
  103.             $keyOrKeyArray,
  104.             empty($header->kid) ? null $header->kid
  105.         );
  106.         if (empty($algorithm)) {
  107.             // Use deprecated "allowed_algs" to determine if the algorithm is supported.
  108.             // This opens up the possibility of an attack in some implementations.
  109.             // @see https://github.com/firebase/php-jwt/issues/351
  110.             if (!\in_array($header->alg$allowed_algs)) {
  111.                 throw new UnexpectedValueException('Algorithm not allowed');
  112.             }
  113.         } else {
  114.             // Check the algorithm
  115.             if (!self::constantTimeEquals($algorithm$header->alg)) {
  116.                 // See issue #351
  117.                 throw new UnexpectedValueException('Incorrect key for this algorithm');
  118.             }
  119.         }
  120.         if ($header->alg === 'ES256' || $header->alg === 'ES384') {
  121.             // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
  122.             $sig self::signatureToDER($sig);
  123.         }
  124.         if (!static::verify("$headb64.$bodyb64"$sig$keyMaterial$header->alg)) {
  125.             throw new SignatureInvalidException('Signature verification failed');
  126.         }
  127.         // Check the nbf if it is defined. This is the time that the
  128.         // token can actually be used. If it's not yet that time, abort.
  129.         if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
  130.             throw new BeforeValidException(
  131.                 'Cannot handle token prior to ' \date(DateTime::ISO8601$payload->nbf)
  132.             );
  133.         }
  134.         // Check that this token has been created before 'now'. This prevents
  135.         // using tokens that have been created for later use (and haven't
  136.         // correctly used the nbf claim).
  137.         if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
  138.             throw new BeforeValidException(
  139.                 'Cannot handle token prior to ' \date(DateTime::ISO8601$payload->iat)
  140.             );
  141.         }
  142.         // Check if this token has expired.
  143.         if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
  144.             throw new ExpiredException('Expired token');
  145.         }
  146.         return $payload;
  147.     }
  148.     /**
  149.      * Converts and signs a PHP object or array into a JWT string.
  150.      *
  151.      * @param object|array      $payload    PHP object or array
  152.      * @param string|resource   $key        The secret key.
  153.      *                                      If the algorithm used is asymmetric, this is the private key
  154.      * @param string            $alg        The signing algorithm.
  155.      *                                      Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
  156.      *                                      'HS512', 'RS256', 'RS384', and 'RS512'
  157.      * @param mixed             $keyId
  158.      * @param array             $head       An array with header elements to attach
  159.      *
  160.      * @return string A signed JWT
  161.      *
  162.      * @uses jsonEncode
  163.      * @uses urlsafeB64Encode
  164.      */
  165.     public static function encode($payload$key$alg 'HS256'$keyId null$head null)
  166.     {
  167.         $header = array('typ' => 'JWT''alg' => $alg);
  168.         if ($keyId !== null) {
  169.             $header['kid'] = $keyId;
  170.         }
  171.         if (isset($head) && \is_array($head)) {
  172.             $header \array_merge($head$header);
  173.         }
  174.         $segments = array();
  175.         $segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
  176.         $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
  177.         $signing_input \implode('.'$segments);
  178.         $signature = static::sign($signing_input$key$alg);
  179.         $segments[] = static::urlsafeB64Encode($signature);
  180.         return \implode('.'$segments);
  181.     }
  182.     /**
  183.      * Sign a string with a given key and algorithm.
  184.      *
  185.      * @param string            $msg    The message to sign
  186.      * @param string|resource   $key    The secret key
  187.      * @param string            $alg    The signing algorithm.
  188.      *                                  Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
  189.      *                                  'HS512', 'RS256', 'RS384', and 'RS512'
  190.      *
  191.      * @return string An encrypted message
  192.      *
  193.      * @throws DomainException Unsupported algorithm or bad key was specified
  194.      */
  195.     public static function sign($msg$key$alg 'HS256')
  196.     {
  197.         if (empty(static::$supported_algs[$alg])) {
  198.             throw new DomainException('Algorithm not supported');
  199.         }
  200.         list($function$algorithm) = static::$supported_algs[$alg];
  201.         switch ($function) {
  202.             case 'hash_hmac':
  203.                 return \hash_hmac($algorithm$msg$keytrue);
  204.             case 'openssl':
  205.                 $signature '';
  206.                 $success \openssl_sign($msg$signature$key$algorithm);
  207.                 if (!$success) {
  208.                     throw new DomainException("OpenSSL unable to sign data");
  209.                 }
  210.                 if ($alg === 'ES256') {
  211.                     $signature self::signatureFromDER($signature256);
  212.                 } elseif ($alg === 'ES384') {
  213.                     $signature self::signatureFromDER($signature384);
  214.                 }
  215.                 return $signature;
  216.             case 'sodium_crypto':
  217.                 if (!function_exists('sodium_crypto_sign_detached')) {
  218.                     throw new DomainException('libsodium is not available');
  219.                 }
  220.                 try {
  221.                     // The last non-empty line is used as the key.
  222.                     $lines array_filter(explode("\n"$key));
  223.                     $key base64_decode(end($lines));
  224.                     return sodium_crypto_sign_detached($msg$key);
  225.                 } catch (Exception $e) {
  226.                     throw new DomainException($e->getMessage(), 0$e);
  227.                 }
  228.         }
  229.     }
  230.     /**
  231.      * Verify a signature with the message, key and method. Not all methods
  232.      * are symmetric, so we must have a separate verify and sign method.
  233.      *
  234.      * @param string            $msg        The original message (header and body)
  235.      * @param string            $signature  The original signature
  236.      * @param string|resource   $key        For HS*, a string key works. for RS*, must be a resource of an openssl public key
  237.      * @param string            $alg        The algorithm
  238.      *
  239.      * @return bool
  240.      *
  241.      * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure
  242.      */
  243.     private static function verify($msg$signature$key$alg)
  244.     {
  245.         if (empty(static::$supported_algs[$alg])) {
  246.             throw new DomainException('Algorithm not supported');
  247.         }
  248.         list($function$algorithm) = static::$supported_algs[$alg];
  249.         switch ($function) {
  250.             case 'openssl':
  251.                 $success \openssl_verify($msg$signature$key$algorithm);
  252.                 if ($success === 1) {
  253.                     return true;
  254.                 } elseif ($success === 0) {
  255.                     return false;
  256.                 }
  257.                 // returns 1 on success, 0 on failure, -1 on error.
  258.                 throw new DomainException(
  259.                     'OpenSSL error: ' \openssl_error_string()
  260.                 );
  261.             case 'sodium_crypto':
  262.               if (!function_exists('sodium_crypto_sign_verify_detached')) {
  263.                   throw new DomainException('libsodium is not available');
  264.               }
  265.               try {
  266.                   // The last non-empty line is used as the key.
  267.                   $lines array_filter(explode("\n"$key));
  268.                   $key base64_decode(end($lines));
  269.                   return sodium_crypto_sign_verify_detached($signature$msg$key);
  270.               } catch (Exception $e) {
  271.                   throw new DomainException($e->getMessage(), 0$e);
  272.               }
  273.             case 'hash_hmac':
  274.             default:
  275.                 $hash \hash_hmac($algorithm$msg$keytrue);
  276.                 return self::constantTimeEquals($signature$hash);
  277.         }
  278.     }
  279.     /**
  280.      * Decode a JSON string into a PHP object.
  281.      *
  282.      * @param string $input JSON string
  283.      *
  284.      * @return object Object representation of JSON string
  285.      *
  286.      * @throws DomainException Provided string was invalid JSON
  287.      */
  288.     public static function jsonDecode($input)
  289.     {
  290.         if (\version_compare(PHP_VERSION'5.4.0''>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE 4)) {
  291.             /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you
  292.              * to specify that large ints (like Steam Transaction IDs) should be treated as
  293.              * strings, rather than the PHP default behaviour of converting them to floats.
  294.              */
  295.             $obj \json_decode($inputfalse512JSON_BIGINT_AS_STRING);
  296.         } else {
  297.             /** Not all servers will support that, however, so for older versions we must
  298.              * manually detect large ints in the JSON string and quote them (thus converting
  299.              *them to strings) before decoding, hence the preg_replace() call.
  300.              */
  301.             $max_int_length \strlen((string) PHP_INT_MAX) - 1;
  302.             $json_without_bigints \preg_replace('/:\s*(-?\d{'.$max_int_length.',})/'': "$1"'$input);
  303.             $obj \json_decode($json_without_bigints);
  304.         }
  305.         if ($errno \json_last_error()) {
  306.             static::handleJsonError($errno);
  307.         } elseif ($obj === null && $input !== 'null') {
  308.             throw new DomainException('Null result with non-null input');
  309.         }
  310.         return $obj;
  311.     }
  312.     /**
  313.      * Encode a PHP object into a JSON string.
  314.      *
  315.      * @param object|array $input A PHP object or array
  316.      *
  317.      * @return string JSON representation of the PHP object or array
  318.      *
  319.      * @throws DomainException Provided object could not be encoded to valid JSON
  320.      */
  321.     public static function jsonEncode($input)
  322.     {
  323.         $json \json_encode($input);
  324.         if ($errno \json_last_error()) {
  325.             static::handleJsonError($errno);
  326.         } elseif ($json === 'null' && $input !== null) {
  327.             throw new DomainException('Null result with non-null input');
  328.         }
  329.         return $json;
  330.     }
  331.     /**
  332.      * Decode a string with URL-safe Base64.
  333.      *
  334.      * @param string $input A Base64 encoded string
  335.      *
  336.      * @return string A decoded string
  337.      */
  338.     public static function urlsafeB64Decode($input)
  339.     {
  340.         $remainder \strlen($input) % 4;
  341.         if ($remainder) {
  342.             $padlen $remainder;
  343.             $input .= \str_repeat('='$padlen);
  344.         }
  345.         return \base64_decode(\strtr($input'-_''+/'));
  346.     }
  347.     /**
  348.      * Encode a string with URL-safe Base64.
  349.      *
  350.      * @param string $input The string you want encoded
  351.      *
  352.      * @return string The base64 encode of what you passed in
  353.      */
  354.     public static function urlsafeB64Encode($input)
  355.     {
  356.         return \str_replace('='''\strtr(\base64_encode($input), '+/''-_'));
  357.     }
  358.     /**
  359.      * Determine if an algorithm has been provided for each Key
  360.      *
  361.      * @param Key|array<Key>|mixed $keyOrKeyArray
  362.      * @param string|null $kid
  363.      *
  364.      * @throws UnexpectedValueException
  365.      *
  366.      * @return array containing the keyMaterial and algorithm
  367.      */
  368.     private static function getKeyMaterialAndAlgorithm($keyOrKeyArray$kid null)
  369.     {
  370.         if (
  371.             is_string($keyOrKeyArray)
  372.             || is_resource($keyOrKeyArray)
  373.             || $keyOrKeyArray instanceof OpenSSLAsymmetricKey
  374.         ) {
  375.             return array($keyOrKeyArraynull);
  376.         }
  377.         if ($keyOrKeyArray instanceof Key) {
  378.             return array($keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm());
  379.         }
  380.         if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) {
  381.             if (!isset($kid)) {
  382.                 throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
  383.             }
  384.             if (!isset($keyOrKeyArray[$kid])) {
  385.                 throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
  386.             }
  387.             $key $keyOrKeyArray[$kid];
  388.             if ($key instanceof Key) {
  389.                 return array($key->getKeyMaterial(), $key->getAlgorithm());
  390.             }
  391.             return array($keynull);
  392.         }
  393.         throw new UnexpectedValueException(
  394.             '$keyOrKeyArray must be a string|resource key, an array of string|resource keys, '
  395.             'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys'
  396.         );
  397.     }
  398.     /**
  399.      * @param string $left
  400.      * @param string $right
  401.      * @return bool
  402.      */
  403.     public static function constantTimeEquals($left$right)
  404.     {
  405.         if (\function_exists('hash_equals')) {
  406.             return \hash_equals($left$right);
  407.         }
  408.         $len \min(static::safeStrlen($left), static::safeStrlen($right));
  409.         $status 0;
  410.         for ($i 0$i $len$i++) {
  411.             $status |= (\ord($left[$i]) ^ \ord($right[$i]));
  412.         }
  413.         $status |= (static::safeStrlen($left) ^ static::safeStrlen($right));
  414.         return ($status === 0);
  415.     }
  416.     /**
  417.      * Helper method to create a JSON error.
  418.      *
  419.      * @param int $errno An error number from json_last_error()
  420.      *
  421.      * @return void
  422.      */
  423.     private static function handleJsonError($errno)
  424.     {
  425.         $messages = array(
  426.             JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
  427.             JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
  428.             JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
  429.             JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
  430.             JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
  431.         );
  432.         throw new DomainException(
  433.             isset($messages[$errno])
  434.             ? $messages[$errno]
  435.             : 'Unknown JSON error: ' $errno
  436.         );
  437.     }
  438.     /**
  439.      * Get the number of bytes in cryptographic strings.
  440.      *
  441.      * @param string $str
  442.      *
  443.      * @return int
  444.      */
  445.     private static function safeStrlen($str)
  446.     {
  447.         if (\function_exists('mb_strlen')) {
  448.             return \mb_strlen($str'8bit');
  449.         }
  450.         return \strlen($str);
  451.     }
  452.     /**
  453.      * Convert an ECDSA signature to an ASN.1 DER sequence
  454.      *
  455.      * @param   string $sig The ECDSA signature to convert
  456.      * @return  string The encoded DER object
  457.      */
  458.     private static function signatureToDER($sig)
  459.     {
  460.         // Separate the signature into r-value and s-value
  461.         list($r$s) = \str_split($sig, (int) (\strlen($sig) / 2));
  462.         // Trim leading zeros
  463.         $r \ltrim($r"\x00");
  464.         $s \ltrim($s"\x00");
  465.         // Convert r-value and s-value from unsigned big-endian integers to
  466.         // signed two's complement
  467.         if (\ord($r[0]) > 0x7f) {
  468.             $r "\x00" $r;
  469.         }
  470.         if (\ord($s[0]) > 0x7f) {
  471.             $s "\x00" $s;
  472.         }
  473.         return self::encodeDER(
  474.             self::ASN1_SEQUENCE,
  475.             self::encodeDER(self::ASN1_INTEGER$r) .
  476.             self::encodeDER(self::ASN1_INTEGER$s)
  477.         );
  478.     }
  479.     /**
  480.      * Encodes a value into a DER object.
  481.      *
  482.      * @param   int     $type DER tag
  483.      * @param   string  $value the value to encode
  484.      * @return  string  the encoded object
  485.      */
  486.     private static function encodeDER($type$value)
  487.     {
  488.         $tag_header 0;
  489.         if ($type === self::ASN1_SEQUENCE) {
  490.             $tag_header |= 0x20;
  491.         }
  492.         // Type
  493.         $der \chr($tag_header $type);
  494.         // Length
  495.         $der .= \chr(\strlen($value));
  496.         return $der $value;
  497.     }
  498.     /**
  499.      * Encodes signature from a DER object.
  500.      *
  501.      * @param   string  $der binary signature in DER format
  502.      * @param   int     $keySize the number of bits in the key
  503.      * @return  string  the signature
  504.      */
  505.     private static function signatureFromDER($der$keySize)
  506.     {
  507.         // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
  508.         list($offset$_) = self::readDER($der);
  509.         list($offset$r) = self::readDER($der$offset);
  510.         list($offset$s) = self::readDER($der$offset);
  511.         // Convert r-value and s-value from signed two's compliment to unsigned
  512.         // big-endian integers
  513.         $r \ltrim($r"\x00");
  514.         $s \ltrim($s"\x00");
  515.         // Pad out r and s so that they are $keySize bits long
  516.         $r \str_pad($r$keySize 8"\x00"STR_PAD_LEFT);
  517.         $s \str_pad($s$keySize 8"\x00"STR_PAD_LEFT);
  518.         return $r $s;
  519.     }
  520.     /**
  521.      * Reads binary DER-encoded data and decodes into a single object
  522.      *
  523.      * @param string $der the binary data in DER format
  524.      * @param int $offset the offset of the data stream containing the object
  525.      * to decode
  526.      * @return array [$offset, $data] the new offset and the decoded object
  527.      */
  528.     private static function readDER($der$offset 0)
  529.     {
  530.         $pos $offset;
  531.         $size \strlen($der);
  532.         $constructed = (\ord($der[$pos]) >> 5) & 0x01;
  533.         $type \ord($der[$pos++]) & 0x1f;
  534.         // Length
  535.         $len \ord($der[$pos++]);
  536.         if ($len 0x80) {
  537.             $n $len 0x1f;
  538.             $len 0;
  539.             while ($n-- && $pos $size) {
  540.                 $len = ($len << 8) | \ord($der[$pos++]);
  541.             }
  542.         }
  543.         // Value
  544.         if ($type == self::ASN1_BIT_STRING) {
  545.             $pos++; // Skip the first contents octet (padding indicator)
  546.             $data \substr($der$pos$len 1);
  547.             $pos += $len 1;
  548.         } elseif (!$constructed) {
  549.             $data \substr($der$pos$len);
  550.             $pos += $len;
  551.         } else {
  552.             $data null;
  553.         }
  554.         return array($pos$data);
  555.     }
  556. }