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: | |