Subversion Repositories oidplus

Rev

Rev 637 | Details | Compare with Previous | Last modification | View Log | RSS feed

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