| 1: | <?php
|
| 2: |
|
| 3: | namespace Firebase\JWT;
|
| 4: |
|
| 5: | use ArrayAccess;
|
| 6: | use DomainException;
|
| 7: | use Exception;
|
| 8: | use InvalidArgumentException;
|
| 9: | use OpenSSLAsymmetricKey;
|
| 10: | use UnexpectedValueException;
|
| 11: | use DateTime;
|
| 12: |
|
| 13: | |
| 14: | |
| 15: | |
| 16: | |
| 17: | |
| 18: | |
| 19: | |
| 20: | |
| 21: | |
| 22: | |
| 23: | |
| 24: | |
| 25: |
|
| 26: | class JWT
|
| 27: | {
|
| 28: |
|
| 29: |
|
| 30: |
|
| 31: | private static $asn1Integer = 0x02;
|
| 32: | private static $asn1Sequence = 0x10;
|
| 33: | private static $asn1BitString = 0x03;
|
| 34: |
|
| 35: | |
| 36: | |
| 37: | |
| 38: | |
| 39: |
|
| 40: | public static $leeway = 0;
|
| 41: |
|
| 42: | |
| 43: | |
| 44: | |
| 45: | |
| 46: | |
| 47: |
|
| 48: | public static $timestamp = null;
|
| 49: |
|
| 50: | public static $supported_algs = array(
|
| 51: | 'ES384' => array('openssl', 'SHA384'),
|
| 52: | 'ES256' => array('openssl', 'SHA256'),
|
| 53: | 'HS256' => array('hash_hmac', 'SHA256'),
|
| 54: | 'HS384' => array('hash_hmac', 'SHA384'),
|
| 55: | 'HS512' => array('hash_hmac', 'SHA512'),
|
| 56: | 'RS256' => array('openssl', 'SHA256'),
|
| 57: | 'RS384' => array('openssl', 'SHA384'),
|
| 58: | 'RS512' => array('openssl', 'SHA512'),
|
| 59: | 'EdDSA' => array('sodium_crypto', 'EdDSA'),
|
| 60: | );
|
| 61: |
|
| 62: | |
| 63: | |
| 64: | |
| 65: | |
| 66: | |
| 67: | |
| 68: | |
| 69: | |
| 70: | |
| 71: | |
| 72: | |
| 73: | |
| 74: | |
| 75: | |
| 76: | |
| 77: | |
| 78: | |
| 79: | |
| 80: | |
| 81: | |
| 82: | |
| 83: | |
| 84: |
|
| 85: | public static function decode($jwt, $keyOrKeyArray)
|
| 86: | {
|
| 87: |
|
| 88: | $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
|
| 89: |
|
| 90: | if (empty($keyOrKeyArray)) {
|
| 91: | throw new InvalidArgumentException('Key may not be empty');
|
| 92: | }
|
| 93: | $tks = \explode('.', $jwt);
|
| 94: | if (\count($tks) != 3) {
|
| 95: | throw new UnexpectedValueException('Wrong number of segments');
|
| 96: | }
|
| 97: | list($headb64, $bodyb64, $cryptob64) = $tks;
|
| 98: | if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
|
| 99: | throw new UnexpectedValueException('Invalid header encoding');
|
| 100: | }
|
| 101: | if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
|
| 102: | throw new UnexpectedValueException('Invalid claims encoding');
|
| 103: | }
|
| 104: | if (false === ($sig = static::urlsafeB64Decode($cryptob64))) {
|
| 105: | throw new UnexpectedValueException('Invalid signature encoding');
|
| 106: | }
|
| 107: | if (empty($header->alg)) {
|
| 108: | throw new UnexpectedValueException('Empty algorithm');
|
| 109: | }
|
| 110: | if (empty(static::$supported_algs[$header->alg])) {
|
| 111: | throw new UnexpectedValueException('Algorithm not supported');
|
| 112: | }
|
| 113: |
|
| 114: | $key = self::getKey($keyOrKeyArray, empty($header->kid) ? null : $header->kid);
|
| 115: |
|
| 116: |
|
| 117: | if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) {
|
| 118: |
|
| 119: | throw new UnexpectedValueException('Incorrect key for this algorithm');
|
| 120: | }
|
| 121: | if ($header->alg === 'ES256' || $header->alg === 'ES384') {
|
| 122: |
|
| 123: | $sig = self::signatureToDER($sig);
|
| 124: | }
|
| 125: | if (!static::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) {
|
| 126: | throw new SignatureInvalidException('Signature verification failed');
|
| 127: | }
|
| 128: |
|
| 129: |
|
| 130: |
|
| 131: | if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
|
| 132: | throw new BeforeValidException(
|
| 133: | 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf)
|
| 134: | );
|
| 135: | }
|
| 136: |
|
| 137: |
|
| 138: |
|
| 139: |
|
| 140: | if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
|
| 141: | throw new BeforeValidException(
|
| 142: | 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat)
|
| 143: | );
|
| 144: | }
|
| 145: |
|
| 146: |
|
| 147: | if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
|
| 148: | throw new ExpiredException('Expired token');
|
| 149: | }
|
| 150: |
|
| 151: | return $payload;
|
| 152: | }
|
| 153: |
|
| 154: | |
| 155: | |
| 156: | |
| 157: | |
| 158: | |
| 159: | |
| 160: | |
| 161: | |
| 162: | |
| 163: | |
| 164: | |
| 165: | |
| 166: | |
| 167: | |
| 168: | |
| 169: | |
| 170: |
|
| 171: | public static function encode($payload, $key, $alg, $keyId = null, $head = null)
|
| 172: | {
|
| 173: | $header = array('typ' => 'JWT', 'alg' => $alg);
|
| 174: | if ($keyId !== null) {
|
| 175: | $header['kid'] = $keyId;
|
| 176: | }
|
| 177: | if (isset($head) && \is_array($head)) {
|
| 178: | $header = \array_merge($head, $header);
|
| 179: | }
|
| 180: | $segments = array();
|
| 181: | $segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
|
| 182: | $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
|
| 183: | $signing_input = \implode('.', $segments);
|
| 184: |
|
| 185: | $signature = static::sign($signing_input, $key, $alg);
|
| 186: | $segments[] = static::urlsafeB64Encode($signature);
|
| 187: |
|
| 188: | return \implode('.', $segments);
|
| 189: | }
|
| 190: |
|
| 191: | |
| 192: | |
| 193: | |
| 194: | |
| 195: | |
| 196: | |
| 197: | |
| 198: | |
| 199: | |
| 200: | |
| 201: | |
| 202: | |
| 203: |
|
| 204: | public static function sign($msg, $key, $alg)
|
| 205: | {
|
| 206: | if (empty(static::$supported_algs[$alg])) {
|
| 207: | throw new DomainException('Algorithm not supported');
|
| 208: | }
|
| 209: | list($function, $algorithm) = static::$supported_algs[$alg];
|
| 210: | switch ($function) {
|
| 211: | case 'hash_hmac':
|
| 212: | return \hash_hmac($algorithm, $msg, $key, true);
|
| 213: | case 'openssl':
|
| 214: | $signature = '';
|
| 215: | $success = \openssl_sign($msg, $signature, $key, $algorithm);
|
| 216: | if (!$success) {
|
| 217: | throw new DomainException("OpenSSL unable to sign data");
|
| 218: | }
|
| 219: | if ($alg === 'ES256') {
|
| 220: | $signature = self::signatureFromDER($signature, 256);
|
| 221: | } elseif ($alg === 'ES384') {
|
| 222: | $signature = self::signatureFromDER($signature, 384);
|
| 223: | }
|
| 224: | return $signature;
|
| 225: | case 'sodium_crypto':
|
| 226: | if (!function_exists('sodium_crypto_sign_detached')) {
|
| 227: | throw new DomainException('libsodium is not available');
|
| 228: | }
|
| 229: | try {
|
| 230: |
|
| 231: | $lines = array_filter(explode("\n", $key));
|
| 232: | $key = base64_decode(end($lines));
|
| 233: | return sodium_crypto_sign_detached($msg, $key);
|
| 234: | } catch (Exception $e) {
|
| 235: | throw new DomainException($e->getMessage(), 0, $e);
|
| 236: | }
|
| 237: | }
|
| 238: | }
|
| 239: |
|
| 240: | |
| 241: | |
| 242: | |
| 243: | |
| 244: | |
| 245: | |
| 246: | |
| 247: | |
| 248: | |
| 249: | |
| 250: | |
| 251: | |
| 252: |
|
| 253: | private static function verify($msg, $signature, $key, $alg)
|
| 254: | {
|
| 255: | if (empty(static::$supported_algs[$alg])) {
|
| 256: | throw new DomainException('Algorithm not supported');
|
| 257: | }
|
| 258: |
|
| 259: | list($function, $algorithm) = static::$supported_algs[$alg];
|
| 260: | switch ($function) {
|
| 261: | case 'openssl':
|
| 262: | $success = \openssl_verify($msg, $signature, $key, $algorithm);
|
| 263: | if ($success === 1) {
|
| 264: | return true;
|
| 265: | } elseif ($success === 0) {
|
| 266: | return false;
|
| 267: | }
|
| 268: |
|
| 269: | throw new DomainException(
|
| 270: | 'OpenSSL error: ' . \openssl_error_string()
|
| 271: | );
|
| 272: | case 'sodium_crypto':
|
| 273: | if (!function_exists('sodium_crypto_sign_verify_detached')) {
|
| 274: | throw new DomainException('libsodium is not available');
|
| 275: | }
|
| 276: | try {
|
| 277: |
|
| 278: | $lines = array_filter(explode("\n", $key));
|
| 279: | $key = base64_decode(end($lines));
|
| 280: | return sodium_crypto_sign_verify_detached($signature, $msg, $key);
|
| 281: | } catch (Exception $e) {
|
| 282: | throw new DomainException($e->getMessage(), 0, $e);
|
| 283: | }
|
| 284: | case 'hash_hmac':
|
| 285: | default:
|
| 286: | $hash = \hash_hmac($algorithm, $msg, $key, true);
|
| 287: | return self::constantTimeEquals($signature, $hash);
|
| 288: | }
|
| 289: | }
|
| 290: |
|
| 291: | |
| 292: | |
| 293: | |
| 294: | |
| 295: | |
| 296: | |
| 297: | |
| 298: | |
| 299: |
|
| 300: | public static function jsonDecode($input)
|
| 301: | {
|
| 302: | if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
|
| 303: | |
| 304: | |
| 305: | |
| 306: |
|
| 307: | $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
|
| 308: | } else {
|
| 309: | |
| 310: | |
| 311: | |
| 312: |
|
| 313: | $max_int_length = \strlen((string) PHP_INT_MAX) - 1;
|
| 314: | $json_without_bigints = \preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input);
|
| 315: | $obj = \json_decode($json_without_bigints);
|
| 316: | }
|
| 317: |
|
| 318: | if ($errno = \json_last_error()) {
|
| 319: | static::handleJsonError($errno);
|
| 320: | } elseif ($obj === null && $input !== 'null') {
|
| 321: | throw new DomainException('Null result with non-null input');
|
| 322: | }
|
| 323: | return $obj;
|
| 324: | }
|
| 325: |
|
| 326: | |
| 327: | |
| 328: | |
| 329: | |
| 330: | |
| 331: | |
| 332: | |
| 333: | |
| 334: |
|
| 335: | public static function jsonEncode($input)
|
| 336: | {
|
| 337: | if (PHP_VERSION_ID >= 50400) {
|
| 338: | $json = \json_encode($input, \JSON_UNESCAPED_SLASHES);
|
| 339: | } else {
|
| 340: |
|
| 341: | $json = \json_encode($input);
|
| 342: | }
|
| 343: | if ($errno = \json_last_error()) {
|
| 344: | static::handleJsonError($errno);
|
| 345: | } elseif ($json === 'null' && $input !== null) {
|
| 346: | throw new DomainException('Null result with non-null input');
|
| 347: | }
|
| 348: | return $json;
|
| 349: | }
|
| 350: |
|
| 351: | |
| 352: | |
| 353: | |
| 354: | |
| 355: | |
| 356: | |
| 357: |
|
| 358: | public static function urlsafeB64Decode($input)
|
| 359: | {
|
| 360: | $remainder = \strlen($input) % 4;
|
| 361: | if ($remainder) {
|
| 362: | $padlen = 4 - $remainder;
|
| 363: | $input .= \str_repeat('=', $padlen);
|
| 364: | }
|
| 365: | return \base64_decode(\strtr($input, '-_', '+/'));
|
| 366: | }
|
| 367: |
|
| 368: | |
| 369: | |
| 370: | |
| 371: | |
| 372: | |
| 373: | |
| 374: |
|
| 375: | public static function urlsafeB64Encode($input)
|
| 376: | {
|
| 377: | return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
|
| 378: | }
|
| 379: |
|
| 380: |
|
| 381: | |
| 382: | |
| 383: | |
| 384: | |
| 385: | |
| 386: | |
| 387: | |
| 388: | |
| 389: | |
| 390: |
|
| 391: | private static function getKey($keyOrKeyArray, $kid = null)
|
| 392: | {
|
| 393: | if ($keyOrKeyArray instanceof Key) {
|
| 394: | return $keyOrKeyArray;
|
| 395: | }
|
| 396: |
|
| 397: | if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) {
|
| 398: | foreach ($keyOrKeyArray as $keyId => $key) {
|
| 399: | if (!$key instanceof Key) {
|
| 400: | throw new UnexpectedValueException(
|
| 401: | '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an '
|
| 402: | . 'array of Firebase\JWT\Key keys'
|
| 403: | );
|
| 404: | }
|
| 405: | }
|
| 406: | if (!isset($kid)) {
|
| 407: | throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
|
| 408: | }
|
| 409: | if (!isset($keyOrKeyArray[$kid])) {
|
| 410: | throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
|
| 411: | }
|
| 412: |
|
| 413: | return $keyOrKeyArray[$kid];
|
| 414: | }
|
| 415: |
|
| 416: | throw new UnexpectedValueException(
|
| 417: | '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an '
|
| 418: | . 'array of Firebase\JWT\Key keys'
|
| 419: | );
|
| 420: | }
|
| 421: |
|
| 422: | |
| 423: | |
| 424: | |
| 425: | |
| 426: |
|
| 427: | public static function constantTimeEquals($left, $right)
|
| 428: | {
|
| 429: | if (\function_exists('hash_equals')) {
|
| 430: | return \hash_equals($left, $right);
|
| 431: | }
|
| 432: | $len = \min(static::safeStrlen($left), static::safeStrlen($right));
|
| 433: |
|
| 434: | $status = 0;
|
| 435: | for ($i = 0; $i < $len; $i++) {
|
| 436: | $status |= (\ord($left[$i]) ^ \ord($right[$i]));
|
| 437: | }
|
| 438: | $status |= (static::safeStrlen($left) ^ static::safeStrlen($right));
|
| 439: |
|
| 440: | return ($status === 0);
|
| 441: | }
|
| 442: |
|
| 443: | |
| 444: | |
| 445: | |
| 446: | |
| 447: | |
| 448: | |
| 449: |
|
| 450: | private static function handleJsonError($errno)
|
| 451: | {
|
| 452: | $messages = array(
|
| 453: | JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
|
| 454: | JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
|
| 455: | JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
|
| 456: | JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
|
| 457: | JSON_ERROR_UTF8 => 'Malformed UTF-8 characters'
|
| 458: | );
|
| 459: | throw new DomainException(
|
| 460: | isset($messages[$errno])
|
| 461: | ? $messages[$errno]
|
| 462: | : 'Unknown JSON error: ' . $errno
|
| 463: | );
|
| 464: | }
|
| 465: |
|
| 466: | |
| 467: | |
| 468: | |
| 469: | |
| 470: | |
| 471: | |
| 472: |
|
| 473: | private static function safeStrlen($str)
|
| 474: | {
|
| 475: | if (\function_exists('mb_strlen')) {
|
| 476: | return \mb_strlen($str, '8bit');
|
| 477: | }
|
| 478: | return \strlen($str);
|
| 479: | }
|
| 480: |
|
| 481: | |
| 482: | |
| 483: | |
| 484: | |
| 485: | |
| 486: |
|
| 487: | private static function signatureToDER($sig)
|
| 488: | {
|
| 489: |
|
| 490: | list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2));
|
| 491: |
|
| 492: |
|
| 493: | $r = \ltrim($r, "\x00");
|
| 494: | $s = \ltrim($s, "\x00");
|
| 495: |
|
| 496: |
|
| 497: |
|
| 498: | if (\ord($r[0]) > 0x7f) {
|
| 499: | $r = "\x00" . $r;
|
| 500: | }
|
| 501: | if (\ord($s[0]) > 0x7f) {
|
| 502: | $s = "\x00" . $s;
|
| 503: | }
|
| 504: |
|
| 505: | return self::encodeDER(
|
| 506: | self::$asn1Sequence,
|
| 507: | self::encodeDER(self::$asn1Integer, $r) .
|
| 508: | self::encodeDER(self::$asn1Integer, $s)
|
| 509: | );
|
| 510: | }
|
| 511: |
|
| 512: | |
| 513: | |
| 514: | |
| 515: | |
| 516: | |
| 517: | |
| 518: |
|
| 519: | private static function encodeDER($type, $value)
|
| 520: | {
|
| 521: | $tag_header = 0;
|
| 522: | if ($type === self::$asn1Sequence) {
|
| 523: | $tag_header |= 0x20;
|
| 524: | }
|
| 525: |
|
| 526: |
|
| 527: | $der = \chr($tag_header | $type);
|
| 528: |
|
| 529: |
|
| 530: | $der .= \chr(\strlen($value));
|
| 531: |
|
| 532: | return $der . $value;
|
| 533: | }
|
| 534: |
|
| 535: | |
| 536: | |
| 537: | |
| 538: | |
| 539: | |
| 540: | |
| 541: |
|
| 542: | private static function signatureFromDER($der, $keySize)
|
| 543: | {
|
| 544: |
|
| 545: | list($offset, $_) = self::readDER($der);
|
| 546: | list($offset, $r) = self::readDER($der, $offset);
|
| 547: | list($offset, $s) = self::readDER($der, $offset);
|
| 548: |
|
| 549: |
|
| 550: |
|
| 551: | $r = \ltrim($r, "\x00");
|
| 552: | $s = \ltrim($s, "\x00");
|
| 553: |
|
| 554: |
|
| 555: | $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
|
| 556: | $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
|
| 557: |
|
| 558: | return $r . $s;
|
| 559: | }
|
| 560: |
|
| 561: | |
| 562: | |
| 563: | |
| 564: | |
| 565: | |
| 566: | |
| 567: | |
| 568: |
|
| 569: | private static function readDER($der, $offset = 0)
|
| 570: | {
|
| 571: | $pos = $offset;
|
| 572: | $size = \strlen($der);
|
| 573: | $constructed = (\ord($der[$pos]) >> 5) & 0x01;
|
| 574: | $type = \ord($der[$pos++]) & 0x1f;
|
| 575: |
|
| 576: |
|
| 577: | $len = \ord($der[$pos++]);
|
| 578: | if ($len & 0x80) {
|
| 579: | $n = $len & 0x1f;
|
| 580: | $len = 0;
|
| 581: | while ($n-- && $pos < $size) {
|
| 582: | $len = ($len << 8) | \ord($der[$pos++]);
|
| 583: | }
|
| 584: | }
|
| 585: |
|
| 586: |
|
| 587: | if ($type == self::$asn1BitString) {
|
| 588: | $pos++;
|
| 589: | $data = \substr($der, $pos, $len - 1);
|
| 590: | $pos += $len - 1;
|
| 591: | } elseif (!$constructed) {
|
| 592: | $data = \substr($der, $pos, $len);
|
| 593: | $pos += $len;
|
| 594: | } else {
|
| 595: | $data = null;
|
| 596: | }
|
| 597: |
|
| 598: | return array($pos, $data);
|
| 599: | }
|
| 600: | }
|
| 601: | |