PluginProbe
Authorizer / 3.6.0
Authorizer v3.6.0
3.16.0 3.15.3 3.15.2 3.15.1 3.15.0 3.14.3 3.14.4 3.14.2 3.14.1 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.9.0 2.9.1 2.9.10 2.9.11 2.9.12 2.9.13 2.9.2 2.9.3 All 127 releases
authorizer / vendor / firebase / php-jwt / src / JWT.php

JWT.php in Authorizer 3.6.0, at vendor/firebase/php-jwt/src/JWT.php

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