PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-jwt / src / class-jwt.php

class-jwt.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at jetpack_vendor/automattic/jetpack-jwt/src/class-jwt.php

444 lines 13.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
9 namespace Automattic\Jetpack;
10
11 use DomainException;
12 use InvalidArgumentException;
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 <[email protected]>
24 * @author Anant Narayanan <[email protected]>
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 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
419 if ( ! class_exists( 'SignatureInvalidException' ) ) {
420 /**
421 * SignatureInvalidException
422 *
423 * @package Automattic\Jetpack\Extensions\Premium_Content
424 */
425 class SignatureInvalidException extends \UnexpectedValueException { }
426 }
427 if ( ! class_exists( 'ExpiredException' ) ) {
428 /**
429 * ExpiredException
430 *
431 * @package Automattic\Jetpack\Extensions\Premium_Content
432 */
433 class ExpiredException extends \UnexpectedValueException { }
434 }
435 if ( ! class_exists( 'BeforeValidException' ) ) {
436 /**
437 * BeforeValidException
438 *
439 * @package Automattic\Jetpack\Extensions\Premium_Content
440 */
441 class BeforeValidException extends \UnexpectedValueException { }
442 }
443 // phpcs:enable
444