Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
60.29% covered (warning)
60.29%
82 / 136
30.00% covered (danger)
30.00%
3 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
JWT
59.40% covered (warning)
59.40%
79 / 133
30.00% covered (danger)
30.00%
3 / 10
338.15
0.00% covered (danger)
0.00%
0 / 1
 decode
54.00% covered (warning)
54.00%
27 / 50
0.00% covered (danger)
0.00%
0 / 1
124.54
 encode
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
4.04
 sign
36.36% covered (danger)
36.36%
4 / 11
0.00% covered (danger)
0.00%
0 / 1
11.44
 verify
31.82% covered (danger)
31.82%
7 / 22
0.00% covered (danger)
0.00%
0 / 1
34.67
 json_decode
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
5.27
 json_encode
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 urlsafe_b64_decode
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 urlsafe_b64_encode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handle_json_error
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 safe_strlen
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
SignatureInvalidException
n/a
0 / 0
n/a
0 / 0
0
n/a
0 / 0
ExpiredException
n/a
0 / 0
n/a
0 / 0
0
n/a
0 / 0
BeforeValidException
n/a
0 / 0
n/a
0 / 0
0
n/a
0 / 0
1<?php
2/**
3 * JSON Web Token implementation, based on this spec:
4 * https://tools.ietf.org/html/rfc7519
5 *
6 * @package automattic/jetpack-jwt
7 */
8
9namespace Automattic\Jetpack;
10
11use DomainException;
12use InvalidArgumentException;
13use 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 */
28class JWT {
29
30    const PACKAGE_VERSION = '0.3.0';
31    /**
32     * When checking nbf, iat or expiration times,
33     * we want to provide some extra leeway time to
34     * account for clock skew.
35     *
36     * @var int $leeway The leeway value.
37     */
38    public static $leeway = 0;
39
40    /**
41     * Allow the current timestamp to be specified.
42     * Useful for fixing a value within unit testing.
43     *
44     * Will default to PHP time() value if null.
45     *
46     * @var string $timestamp The timestamp.
47     */
48    public static $timestamp = null;
49
50    /**
51     * Supported algorithms.
52     *
53     * @var array $supported_algs Supported algorithms.
54     */
55    public static $supported_algs = array(
56        'HS256' => array( 'hash_hmac', 'SHA256' ),
57        'HS512' => array( 'hash_hmac', 'SHA512' ),
58        'HS384' => array( 'hash_hmac', 'SHA384' ),
59        'RS256' => array( 'openssl', 'SHA256' ),
60        'RS384' => array( 'openssl', 'SHA384' ),
61        'RS512' => array( 'openssl', 'SHA512' ),
62    );
63
64    /**
65     * Decodes a JWT string into a PHP object.
66     *
67     * @param string       $jwt            The JWT.
68     * @param string|array $key            The key, or map of keys.
69     *                                     If the algorithm used is asymmetric, this is the public key.
70     * @param array        $allowed_algs   List of supported verification algorithms.
71     *                                     Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'.
72     * @param bool         $as_array Whether to return the result as an associative array.
73     *
74     * @return object|array The JWT's payload as a PHP object or array.
75     *
76     * @throws UnexpectedValueException     Provided JWT was invalid.
77     * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed.
78     * @throws InvalidArgumentException     Provided JWT is trying to be used before it's eligible as defined by 'nbf'.
79     * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'.
80     * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim.
81     *
82     * @uses json_decode
83     * @uses urlsafe_b64_decode
84     */
85    public static function decode( $jwt, $key, array $allowed_algs = array(), $as_array = false ) {
86        $timestamp = static::$timestamp === null ? time() : static::$timestamp;
87
88        if ( empty( $key ) ) {
89            throw new InvalidArgumentException( 'Key may not be empty' );
90        }
91
92        $tks = explode( '.', $jwt );
93        if ( count( $tks ) !== 3 ) {
94            throw new UnexpectedValueException( 'Wrong number of segments' );
95        }
96
97        list( $headb64, $bodyb64, $cryptob64 ) = $tks;
98
99        $header = static::json_decode( static::urlsafe_b64_decode( $headb64 ) );
100        if ( null === $header ) {
101            throw new UnexpectedValueException( 'Invalid header encoding' );
102        }
103
104        $payload = static::json_decode( static::urlsafe_b64_decode( $bodyb64 ), $as_array );
105        if ( null === $payload ) {
106            throw new UnexpectedValueException( 'Invalid claims encoding' );
107        }
108
109        $sig = static::urlsafe_b64_decode( $cryptob64 );
110        if ( false === $sig ) {
111            throw new UnexpectedValueException( 'Invalid signature encoding' );
112        }
113
114        if ( empty( $header->alg ) ) {
115            throw new UnexpectedValueException( 'Empty algorithm' );
116        }
117
118        if ( empty( static::$supported_algs[ $header->alg ] ) ) {
119            throw new UnexpectedValueException( 'Algorithm not supported' );
120        }
121
122        if ( ! in_array( $header->alg, $allowed_algs, true ) ) {
123            throw new UnexpectedValueException( 'Algorithm not allowed' );
124        }
125
126        if ( is_array( $key ) || $key instanceof \ArrayAccess ) {
127            if ( isset( $header->kid ) ) {
128                if ( ! isset( $key[ $header->kid ] ) ) {
129                    throw new UnexpectedValueException( '"kid" invalid, unable to lookup correct key' );
130                }
131                $key = $key[ $header->kid ];
132            } else {
133                throw new UnexpectedValueException( '"kid" empty, unable to lookup correct key' );
134            }
135        }
136
137        // Check the signature.
138        if ( ! static::verify( "$headb64.$bodyb64", $sig, $key, $header->alg ) ) {
139            throw new SignatureInvalidException( 'Signature verification failed' );
140        }
141
142        $nbf = ! $as_array && isset( $payload->nbf ) ? $payload->nbf : null;
143        if ( $as_array && isset( $payload['nbf'] ) ) {
144            $nbf = $payload['nbf'];
145        }
146        // Check if the nbf if it is defined. This is the time that the
147        // token can actually be used. If it's not yet that time, abort.
148        if ( $nbf > ( $timestamp + static::$leeway ) ) {
149            throw new BeforeValidException(
150                'Cannot handle token prior to ' . gmdate( 'Y-m-d\\TH:i:sO', $nbf )
151            );
152        }
153
154        $iat = ! $as_array && isset( $payload->iat ) ? $payload->iat : null;
155        if ( $as_array && isset( $payload['iat'] ) ) {
156            $iat = $payload['iat'];
157        }
158        // Check that this token has been created before 'now'. This prevents
159        // using tokens that have been created for later use (and haven't
160        // correctly used the nbf claim).
161        if ( $iat > ( $timestamp + static::$leeway ) ) {
162            throw new BeforeValidException(
163                'Cannot handle token prior to ' . gmdate( 'Y-m-d\\TH:i:sO', $iat )
164            );
165        }
166
167        $exp = ! $as_array && isset( $payload->exp ) ? $payload->exp : null;
168        if ( $as_array && isset( $payload['exp'] ) ) {
169            $exp = $payload['exp'];
170        }
171
172        // Check if this token has expired.
173        if ( $exp && ( $timestamp - static::$leeway ) >= $exp ) {
174            throw new ExpiredException( 'Expired token' );
175        }
176
177        return $payload;
178    }
179
180    /**
181     * Converts and signs a PHP object or array into a JWT string.
182     *
183     * @param object|array $payload    PHP object or array.
184     * @param string       $key        The secret key.
185     *                                 If the algorithm used is asymmetric, this is the private key.
186     * @param string       $alg        The signing algorithm.
187     *                                 Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'.
188     * @param mixed        $key_id     The key ID.
189     * @param array        $head       An array with header elements to attach.
190     *
191     * @return string A signed JWT
192     *
193     * @uses json_encode
194     * @uses urlsafe_b64_decode
195     */
196    public static function encode( $payload, $key, $alg = 'HS256', $key_id = null, $head = null ) {
197        $header = array(
198            'typ' => 'JWT',
199            'alg' => $alg,
200        );
201
202        if ( null !== $key_id ) {
203            $header['kid'] = $key_id;
204        }
205
206        if ( isset( $head ) && is_array( $head ) ) {
207            $header = array_merge( $head, $header );
208        }
209
210        $segments      = array();
211        $segments[]    = static::urlsafe_b64_encode( static::json_encode( $header ) );
212        $segments[]    = static::urlsafe_b64_encode( static::json_encode( $payload ) );
213        $signing_input = implode( '.', $segments );
214
215        $signature  = static::sign( $signing_input, $key, $alg );
216        $segments[] = static::urlsafe_b64_encode( $signature );
217
218        return implode( '.', $segments );
219    }
220
221    /**
222     * Sign a string with a given key and algorithm.
223     *
224     * @param string          $msg    The message to sign.
225     * @param string|resource $key    The secret key.
226     * @param string          $alg    The signing algorithm.
227     *                                Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'.
228     *
229     * @return string An encrypted message
230     *
231     * @throws DomainException Unsupported algorithm was specified.
232     */
233    public static function sign( $msg, $key, $alg = 'HS256' ) {
234        if ( empty( static::$supported_algs[ $alg ] ) ) {
235            throw new DomainException( 'Algorithm not supported' );
236        }
237        list($function, $algorithm) = static::$supported_algs[ $alg ];
238        switch ( $function ) {
239            case 'hash_hmac':
240                return hash_hmac( $algorithm, $msg, $key, true );
241            case 'openssl':
242                $signature = '';
243                $success   = openssl_sign( $msg, $signature, $key, $algorithm );
244                if ( ! $success ) {
245                    throw new DomainException( 'OpenSSL unable to sign data' );
246                } else {
247                    return $signature;
248                }
249        }
250    }
251
252    /**
253     * Verify a signature with the message, key and method. Not all methods
254     * are symmetric, so we must have a separate verify and sign method.
255     *
256     * @param string          $msg        The original message (header and body).
257     * @param string          $signature  The original signature.
258     * @param string|resource $key        For HS*, a string key works. for RS*, must be a resource of an openssl public key.
259     * @param string          $alg        The algorithm.
260     *
261     * @return bool
262     *
263     * @throws DomainException Invalid Algorithm or OpenSSL failure.
264     */
265    private static function verify( $msg, $signature, $key, $alg ) {
266        if ( empty( static::$supported_algs[ $alg ] ) ) {
267            throw new DomainException( 'Algorithm not supported' );
268        }
269
270        list($function, $algorithm) = static::$supported_algs[ $alg ];
271        switch ( $function ) {
272            case 'openssl':
273                $success = openssl_verify( $msg, $signature, $key, $algorithm );
274
275                if ( 1 === $success ) {
276                    return true;
277                } elseif ( 0 === $success ) {
278                    return false;
279                }
280
281                // returns 1 on success, 0 on failure, -1 on error.
282                throw new DomainException(
283                    'OpenSSL error: ' . openssl_error_string()
284                );
285            case 'hash_hmac':
286            default:
287                $hash = hash_hmac( $algorithm, $msg, $key, true );
288
289                if ( function_exists( 'hash_equals' ) ) {
290                    return hash_equals( $signature, $hash );
291                }
292
293                $len = min( static::safe_strlen( $signature ), static::safe_strlen( $hash ) );
294
295                $status = 0;
296
297                for ( $i = 0; $i < $len; $i++ ) {
298                    $status |= ( ord( $signature[ $i ] ) ^ ord( $hash[ $i ] ) );
299                }
300
301                $status |= ( static::safe_strlen( $signature ) ^ static::safe_strlen( $hash ) );
302
303                return ( 0 === $status );
304        }
305    }
306
307    /**
308     * Decode a JSON string into a PHP object.
309     *
310     * @param string $input JSON string.
311     * @param bool   $as_array Whether to return the result as an associative array.
312     *
313     * @return object|array Object or Array representation of JSON string
314     *
315     * @throws DomainException Provided string was invalid JSON.
316     */
317    public static function json_decode( $input, $as_array = false ) {
318        $obj   = json_decode( $input, $as_array, 512, JSON_BIGINT_AS_STRING );
319        $errno = json_last_error();
320
321        if ( $errno ) {
322            static::handle_json_error( $errno );
323        } elseif ( null === $obj && 'null' !== $input ) {
324            throw new DomainException( 'Null result with non-null input' );
325        } elseif ( $obj === null ) {
326            throw new DomainException( 'Null result' );
327        }
328
329        return $obj;
330    }
331
332    /**
333     * Encode a PHP object into a JSON string.
334     *
335     * @param object|array $input A PHP object or array.
336     *
337     * @return string JSON representation of the PHP object or array.
338     *
339     * @throws DomainException Provided object could not be encoded to valid JSON.
340     */
341    public static function json_encode( $input ) {
342        $json  = \wp_json_encode( $input, JSON_UNESCAPED_SLASHES );
343        $errno = json_last_error();
344
345        if ( $errno ) {
346            static::handle_json_error( $errno );
347        } elseif ( 'null' === $json && null !== $input ) {
348            throw new DomainException( 'Null result with non-null input' );
349        }
350        return $json;
351    }
352
353    /**
354     * Decode a string with URL-safe Base64.
355     *
356     * @param string $input A Base64 encoded string.
357     *
358     * @return string A decoded string
359     */
360    public static function urlsafe_b64_decode( $input ) {
361        $remainder = strlen( $input ) % 4;
362        if ( $remainder ) {
363            $padlen = 4 - $remainder;
364            $input .= str_repeat( '=', $padlen );
365        }
366        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
367        return base64_decode( strtr( $input, '-_', '+/' ) );
368    }
369
370    /**
371     * Encode a string with URL-safe Base64.
372     *
373     * @param string $input The string you want encoded.
374     *
375     * @return string The base64 encode of what you passed in
376     */
377    public static function urlsafe_b64_encode( $input ) {
378        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
379        return str_replace( '=', '', strtr( base64_encode( $input ), '+/', '-_' ) );
380    }
381
382    /**
383     * Helper method to create a JSON error.
384     *
385     * @param int $errno An error number from json_last_error().
386     * @throws DomainException .
387     *
388     * @return never
389     */
390    private static function handle_json_error( $errno ) {
391        $messages = array(
392            JSON_ERROR_DEPTH          => 'Maximum stack depth exceeded',
393            JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
394            JSON_ERROR_CTRL_CHAR      => 'Unexpected control character found',
395            JSON_ERROR_SYNTAX         => 'Syntax error, malformed JSON',
396            JSON_ERROR_UTF8           => 'Malformed UTF-8 characters',
397        );
398        throw new DomainException(
399            $messages[ $errno ] ?? 'Unknown JSON error: ' . $errno
400        );
401    }
402
403    /**
404     * Get the number of bytes in cryptographic strings.
405     *
406     * @param string $str .
407     *
408     * @return int
409     */
410    private static function safe_strlen( $str ) {
411        if ( function_exists( 'mb_strlen' ) ) {
412            return mb_strlen( $str, '8bit' );
413        }
414        return strlen( $str );
415    }
416}
417
418// phpcs:disable
419if ( ! class_exists( 'SignatureInvalidException' ) ) {
420    /**
421     * SignatureInvalidException
422     *
423     * @package Automattic\Jetpack\Extensions\Premium_Content
424     */
425    class SignatureInvalidException extends \UnexpectedValueException { }
426}
427if ( ! class_exists( 'ExpiredException' ) ) {
428    /**
429     * ExpiredException
430     *
431     * @package Automattic\Jetpack\Extensions\Premium_Content
432     */
433    class ExpiredException extends \UnexpectedValueException { }
434}
435if ( ! class_exists( 'BeforeValidException' ) ) {
436    /**
437     * BeforeValidException
438     *
439     * @package Automattic\Jetpack\Extensions\Premium_Content
440     */
441    class BeforeValidException extends \UnexpectedValueException { }
442}
443// phpcs:enable