ILIAS  release_8 Revision v8.24
JWT.php
Go to the documentation of this file.
1<?php
2
3namespace Firebase\JWT;
4
5use ArrayAccess;
6use DateTime;
7use DomainException;
8use Exception;
9use InvalidArgumentException;
10use OpenSSLAsymmetricKey;
11use OpenSSLCertificate;
12use stdClass;
13use UnexpectedValueException;
14
28class JWT
29{
30 private const ASN1_INTEGER = 0x02;
31 private const ASN1_SEQUENCE = 0x10;
32 private const ASN1_BIT_STRING = 0x03;
33
41 public static int $leeway = 0;
42
50 public static ?int $timestamp = null;
51
55 public static array $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
96 public static function decode(
97 string $jwt,
98 $keyOrKeyArray,
99 stdClass &$headers = null
100 ): stdClass {
101 // Validate JWT
102 $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
103
104 if (empty($keyOrKeyArray)) {
105 throw new InvalidArgumentException('Key may not be empty');
106 }
107 $tks = \explode('.', $jwt);
108 if (\count($tks) !== 3) {
109 throw new UnexpectedValueException('Wrong number of segments');
110 }
111 list($headb64, $bodyb64, $cryptob64) = $tks;
112 $headerRaw = static::urlsafeB64Decode($headb64);
113 if (null === ($header = static::jsonDecode($headerRaw))) {
114 throw new UnexpectedValueException('Invalid header encoding');
115 }
116 if ($headers !== null) {
117 $headers = $header;
118 }
119 $payloadRaw = static::urlsafeB64Decode($bodyb64);
120 if (null === ($payload = static::jsonDecode($payloadRaw))) {
121 throw new UnexpectedValueException('Invalid claims encoding');
122 }
123 if (\is_array($payload)) {
124 // prevent PHP Fatal Error in edge-cases when payload is empty array
126 }
127 if (!$payload instanceof stdClass) {
128 throw new UnexpectedValueException('Payload must be a JSON object');
129 }
130 $sig = static::urlsafeB64Decode($cryptob64);
131 if (empty($header->alg)) {
132 throw new UnexpectedValueException('Empty algorithm');
133 }
134 if (empty(static::$supported_algs[$header->alg])) {
135 throw new UnexpectedValueException('Algorithm not supported');
136 }
137
138 $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null);
139
140 // Check the algorithm
141 if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) {
142 // See issue #351
143 throw new UnexpectedValueException('Incorrect key for this algorithm');
144 }
145 if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) {
146 // OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures
147 $sig = self::signatureToDER($sig);
148 }
149 if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) {
150 throw new SignatureInvalidException('Signature verification failed');
151 }
152
153 // Check the nbf if it is defined. This is the time that the
154 // token can actually be used. If it's not yet that time, abort.
155 if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) {
156 $ex = new BeforeValidException(
157 'Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) $payload->nbf)
158 );
159 $ex->setPayload($payload);
160 throw $ex;
161 }
162
163 // Check that this token has been created before 'now'. This prevents
164 // using tokens that have been created for later use (and haven't
165 // correctly used the nbf claim).
166 if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) {
167 $ex = new BeforeValidException(
168 'Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) $payload->iat)
169 );
170 $ex->setPayload($payload);
171 throw $ex;
172 }
173
174 // Check if this token has expired.
175 if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
176 $ex = new ExpiredException('Expired token');
177 $ex->setPayload($payload);
178 throw $ex;
179 }
180
181 return $payload;
182 }
183
199 public static function encode(
200 array $payload,
201 $key,
202 string $alg,
203 string $keyId = null,
204 array $head = null
205 ): string {
206 $header = ['typ' => 'JWT', 'alg' => $alg];
207 if ($keyId !== null) {
208 $header['kid'] = $keyId;
209 }
210 if (isset($head) && \is_array($head)) {
211 $header = \array_merge($head, $header);
212 }
213 $segments = [];
214 $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header));
215 $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload));
216 $signing_input = \implode('.', $segments);
217
218 $signature = static::sign($signing_input, $key, $alg);
219 $segments[] = static::urlsafeB64Encode($signature);
220
221 return \implode('.', $segments);
222 }
223
236 public static function sign(
237 string $msg,
238 $key,
239 string $alg
240 ): string {
241 if (empty(static::$supported_algs[$alg])) {
242 throw new DomainException('Algorithm not supported');
243 }
244 list($function, $algorithm) = static::$supported_algs[$alg];
245 switch ($function) {
246 case 'hash_hmac':
247 if (!\is_string($key)) {
248 throw new InvalidArgumentException('key must be a string when using hmac');
249 }
250 return \hash_hmac($algorithm, $msg, $key, true);
251 case 'openssl':
252 $signature = '';
253 $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line
254 if (!$success) {
255 throw new DomainException('OpenSSL unable to sign data');
256 }
257 if ($alg === 'ES256' || $alg === 'ES256K') {
258 $signature = self::signatureFromDER($signature, 256);
259 } elseif ($alg === 'ES384') {
260 $signature = self::signatureFromDER($signature, 384);
261 }
262 return $signature;
263 case 'sodium_crypto':
264 if (!\function_exists('sodium_crypto_sign_detached')) {
265 throw new DomainException('libsodium is not available');
266 }
267 if (!\is_string($key)) {
268 throw new InvalidArgumentException('key must be a string when using EdDSA');
269 }
270 try {
271 // The last non-empty line is used as the key.
272 $lines = array_filter(explode("\n", $key));
273 $key = base64_decode((string) end($lines));
274 if (\strlen($key) === 0) {
275 throw new DomainException('Key cannot be empty string');
276 }
277 return sodium_crypto_sign_detached($msg, $key);
278 } catch (Exception $e) {
279 throw new DomainException($e->getMessage(), 0, $e);
280 }
281 }
282
283 throw new DomainException('Algorithm not supported');
284 }
285
299 private static function verify(
300 string $msg,
301 string $signature,
302 $keyMaterial,
303 string $alg
304 ): bool {
305 if (empty(static::$supported_algs[$alg])) {
306 throw new DomainException('Algorithm not supported');
307 }
308
309 list($function, $algorithm) = static::$supported_algs[$alg];
310 switch ($function) {
311 case 'openssl':
312 $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line
313 if ($success === 1) {
314 return true;
315 }
316 if ($success === 0) {
317 return false;
318 }
319 // returns 1 on success, 0 on failure, -1 on error.
320 throw new DomainException(
321 'OpenSSL error: ' . \openssl_error_string()
322 );
323 case 'sodium_crypto':
324 if (!\function_exists('sodium_crypto_sign_verify_detached')) {
325 throw new DomainException('libsodium is not available');
326 }
327 if (!\is_string($keyMaterial)) {
328 throw new InvalidArgumentException('key must be a string when using EdDSA');
329 }
330 try {
331 // The last non-empty line is used as the key.
332 $lines = array_filter(explode("\n", $keyMaterial));
333 $key = base64_decode((string) end($lines));
334 if (\strlen($key) === 0) {
335 throw new DomainException('Key cannot be empty string');
336 }
337 if (\strlen($signature) === 0) {
338 throw new DomainException('Signature cannot be empty string');
339 }
340 return sodium_crypto_sign_verify_detached($signature, $msg, $key);
341 } catch (Exception $e) {
342 throw new DomainException($e->getMessage(), 0, $e);
343 }
344 case 'hash_hmac':
345 default:
346 if (!\is_string($keyMaterial)) {
347 throw new InvalidArgumentException('key must be a string when using hmac');
348 }
349 $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
350 return self::constantTimeEquals($hash, $signature);
351 }
352 }
353
363 public static function jsonDecode(string $input)
364 {
365 $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
366
367 if ($errno = \json_last_error()) {
368 self::handleJsonError($errno);
369 } elseif ($obj === null && $input !== 'null') {
370 throw new DomainException('Null result with non-null input');
371 }
372 return $obj;
373 }
374
384 public static function jsonEncode(array $input): string
385 {
386 if (PHP_VERSION_ID >= 50400) {
387 $json = \json_encode($input, \JSON_UNESCAPED_SLASHES);
388 } else {
389 // PHP 5.3 only
390 $json = \json_encode($input);
391 }
392 if ($errno = \json_last_error()) {
393 self::handleJsonError($errno);
394 } elseif ($json === 'null') {
395 throw new DomainException('Null result with non-null input');
396 }
397 if ($json === false) {
398 throw new DomainException('Provided object could not be encoded to valid JSON');
399 }
400 return $json;
401 }
402
412 public static function urlsafeB64Decode(string $input): string
413 {
414 return \base64_decode(self::convertBase64UrlToBase64($input));
415 }
416
427 public static function convertBase64UrlToBase64(string $input): string
428 {
429 $remainder = \strlen($input) % 4;
430 if ($remainder) {
431 $padlen = 4 - $remainder;
432 $input .= \str_repeat('=', $padlen);
433 }
434 return \strtr($input, '-_', '+/');
435 }
436
444 public static function urlsafeB64Encode(string $input): string
445 {
446 return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
447 }
448
449
460 private static function getKey(
461 $keyOrKeyArray,
462 ?string $kid
463 ): Key {
464 if ($keyOrKeyArray instanceof Key) {
465 return $keyOrKeyArray;
466 }
467
468 if (empty($kid) && $kid !== '0') {
469 throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
470 }
471
472 if ($keyOrKeyArray instanceof CachedKeySet) {
473 // Skip "isset" check, as this will automatically refresh if not set
474 return $keyOrKeyArray[$kid];
475 }
476
477 if (!isset($keyOrKeyArray[$kid])) {
478 throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
479 }
480
481 return $keyOrKeyArray[$kid];
482 }
483
489 public static function constantTimeEquals(string $left, string $right): bool
490 {
491 if (\function_exists('hash_equals')) {
492 return \hash_equals($left, $right);
493 }
494 $len = \min(self::safeStrlen($left), self::safeStrlen($right));
495
496 $status = 0;
497 for ($i = 0; $i < $len; $i++) {
498 $status |= (\ord($left[$i]) ^ \ord($right[$i]));
499 }
500 $status |= (self::safeStrlen($left) ^ self::safeStrlen($right));
501
502 return ($status === 0);
503 }
504
514 private static function handleJsonError(int $errno): void
515 {
516 $messages = [
517 JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
518 JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
519 JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
520 JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
521 JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
522 ];
523 throw new DomainException(
524 isset($messages[$errno])
525 ? $messages[$errno]
526 : 'Unknown JSON error: ' . $errno
527 );
528 }
529
537 private static function safeStrlen(string $str): int
538 {
539 if (\function_exists('mb_strlen')) {
540 return \mb_strlen($str, '8bit');
541 }
542 return \strlen($str);
543 }
544
551 private static function signatureToDER(string $sig): string
552 {
553 // Separate the signature into r-value and s-value
554 $length = max(1, (int) (\strlen($sig) / 2));
555 list($r, $s) = \str_split($sig, $length);
556
557 // Trim leading zeros
558 $r = \ltrim($r, "\x00");
559 $s = \ltrim($s, "\x00");
560
561 // Convert r-value and s-value from unsigned big-endian integers to
562 // signed two's complement
563 if (\ord($r[0]) > 0x7f) {
564 $r = "\x00" . $r;
565 }
566 if (\ord($s[0]) > 0x7f) {
567 $s = "\x00" . $s;
568 }
569
570 return self::encodeDER(
571 self::ASN1_SEQUENCE,
572 self::encodeDER(self::ASN1_INTEGER, $r) .
573 self::encodeDER(self::ASN1_INTEGER, $s)
574 );
575 }
576
585 private static function encodeDER(int $type, string $value): string
586 {
587 $tag_header = 0;
588 if ($type === self::ASN1_SEQUENCE) {
589 $tag_header |= 0x20;
590 }
591
592 // Type
593 $der = \chr($tag_header | $type);
594
595 // Length
596 $der .= \chr(\strlen($value));
597
598 return $der . $value;
599 }
600
609 private static function signatureFromDER(string $der, int $keySize): string
610 {
611 // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
612 list($offset, $_) = self::readDER($der);
613 list($offset, $r) = self::readDER($der, $offset);
614 list($offset, $s) = self::readDER($der, $offset);
615
616 // Convert r-value and s-value from signed two's compliment to unsigned
617 // big-endian integers
618 $r = \ltrim($r, "\x00");
619 $s = \ltrim($s, "\x00");
620
621 // Pad out r and s so that they are $keySize bits long
622 $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
623 $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
624
625 return $r . $s;
626 }
627
637 private static function readDER(string $der, int $offset = 0): array
638 {
639 $pos = $offset;
640 $size = \strlen($der);
641 $constructed = (\ord($der[$pos]) >> 5) & 0x01;
642 $type = \ord($der[$pos++]) & 0x1f;
643
644 // Length
645 $len = \ord($der[$pos++]);
646 if ($len & 0x80) {
647 $n = $len & 0x1f;
648 $len = 0;
649 while ($n-- && $pos < $size) {
650 $len = ($len << 8) | \ord($der[$pos++]);
651 }
652 }
653
654 // Value
655 if ($type === self::ASN1_BIT_STRING) {
656 $pos++; // Skip the first contents octet (padding indicator)
657 $data = \substr($der, $pos, $len - 1);
658 $pos += $len - 1;
659 } elseif (!$constructed) {
660 $data = \substr($der, $pos, $len);
661 $pos += $len;
662 } else {
663 $data = null;
664 }
665
666 return [$pos, $data];
667 }
668}
static signatureToDER(string $sig)
Convert an ECDSA signature to an ASN.1 DER sequence.
Definition: JWT.php:551
const ASN1_SEQUENCE
Definition: JWT.php:31
static int $leeway
Definition: JWT.php:41
static handleJsonError(int $errno)
Helper method to create a JSON error.
Definition: JWT.php:514
static encode(array $payload, $key, string $alg, string $keyId=null, array $head=null)
Converts and signs a PHP array into a JWT string.
Definition: JWT.php:199
static verify(string $msg, string $signature, $keyMaterial, string $alg)
Verify a signature with the message, key and method.
Definition: JWT.php:299
static constantTimeEquals(string $left, string $right)
Definition: JWT.php:489
static signatureFromDER(string $der, int $keySize)
Encodes signature from a DER object.
Definition: JWT.php:609
static sign(string $msg, $key, string $alg)
Sign a string with a given key and algorithm.
Definition: JWT.php:236
static decode(string $jwt, $keyOrKeyArray, stdClass &$headers=null)
Decodes a JWT string into a PHP object.
Definition: JWT.php:96
static urlsafeB64Encode(string $input)
Encode a string with URL-safe Base64.
Definition: JWT.php:444
static encodeDER(int $type, string $value)
Encodes a value into a DER object.
Definition: JWT.php:585
const ASN1_BIT_STRING
Definition: JWT.php:32
static int $timestamp
Definition: JWT.php:50
static array $supported_algs
Definition: JWT.php:55
static safeStrlen(string $str)
Get the number of bytes in cryptographic strings.
Definition: JWT.php:537
static getKey( $keyOrKeyArray, ?string $kid)
Determine if an algorithm has been provided for each Key.
Definition: JWT.php:460
static jsonEncode(array $input)
Encode a PHP array into a JSON string.
Definition: JWT.php:384
static readDER(string $der, int $offset=0)
Reads binary DER-encoded data and decodes into a single object.
Definition: JWT.php:637
static jsonDecode(string $input)
Decode a JSON string into a PHP object.
Definition: JWT.php:363
static convertBase64UrlToBase64(string $input)
Convert a string in the base64url (URL-safe Base64) encoding to standard base64.
Definition: JWT.php:427
static urlsafeB64Decode(string $input)
Decode a string with URL-safe Base64.
Definition: JWT.php:412
const ASN1_INTEGER
Definition: JWT.php:30
if(!file_exists(getcwd() . '/ilias.ini.php'))
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: confirmReg.php:20
if(count($parts) !=3) $payload
Definition: ltitoken.php:70
$i
Definition: metadata.php:41
ClientInterface $jwt
JWT object, if any.
Definition: System.php:165
string $kid
Key ID.
Definition: System.php:88
string $key
Consumer key/client ID value.
Definition: System.php:193
$type
$messages
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: xapiexit.php:22