PluginProbe
Authorizer / 3.13.0
Authorizer v3.13.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 2.9.6 All 126 releases
authorizer / vendor / firebase / php-jwt / src / JWT.php

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

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