Completed
Pull Request — master (#131)
by Jesus
03:22
created

JWT::urlsafeB64Encode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Firebase\JWT;
4
use \DomainException;
5
use \InvalidArgumentException;
6
use \UnexpectedValueException;
7
use \DateTime;
8
9
/**
10
 * JSON Web Token implementation, based on this spec:
11
 * http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06
12
 *
13
 * PHP version 5
14
 *
15
 * @category Authentication
16
 * @package  Authentication_JWT
17
 * @author   Neuman Vong <[email protected]>
18
 * @author   Anant Narayanan <[email protected]>
19
 * @license  http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
20
 * @link     https://github.com/firebase/php-jwt
21
 */
22
class JWT
23
{
24
25
    /**
26
     * When checking nbf, iat or expiration times,
27
     * we want to provide some extra leeway time to
28
     * account for clock skew.
29
     */
30
    public static $leeway = 0;
31
32
    /**
33
     * Allow the current timestamp to be specified.
34
     * Useful for fixing a value within unit testing.
35
     *
36
     * Will default to PHP time() value if null.
37
     */
38
    public static $timestamp = null;
39
40
    public static $supported_algs = array(
41
        'HS256' => array('hash_hmac', 'SHA256'),
42
        'HS512' => array('hash_hmac', 'SHA512'),
43
        'HS384' => array('hash_hmac', 'SHA384'),
44
        'RS256' => array('openssl', 'SHA256'),
45
    );
46
47
    /**
48
     * Decodes a JWT string into a PHP object.
49
     *
50
     * @param string        $jwt            The JWT
51
     * @param string|array  $key            The key, or map of keys.
52
     *                                      If the algorithm used is asymmetric, this is the public key
53
     * @param array         $allowed_algs   List of supported verification algorithms
54
     *                                      Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
55
     *
56
     * @return object The JWT's payload as a PHP object
57
     *
58
     * @throws UnexpectedValueException     Provided JWT was invalid
59
     * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
60
     * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
61
     * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
62
     * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
63
     *
64
     * @uses jsonDecode
65
     * @uses urlsafeB64Decode
66
     */
67
    public static function decode($jwt, $key, $allowed_algs = array())
68
    {
69
        $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp;
70
71
        if (empty($key)) {
72
            throw new InvalidArgumentException('Key may not be empty');
73
        }
74
        if (!is_array($allowed_algs)) {
75
            throw new InvalidArgumentException('Algorithm not allowed');
76
        }
77
        $tks = explode('.', $jwt);
78
        if (count($tks) != 3) {
79
            throw new UnexpectedValueException('Wrong number of segments');
80
        }
81
        list($headb64, $bodyb64, $cryptob64) = $tks;
82
        if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
83
            throw new UnexpectedValueException('Invalid header encoding');
84
        }
85
        if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
86
            throw new UnexpectedValueException('Invalid claims encoding');
87
        }
88
        $sig = static::urlsafeB64Decode($cryptob64);
89
        
90
        if (empty($header->alg)) {
91
            throw new UnexpectedValueException('Empty algorithm');
92
        }
93
        if (empty(static::$supported_algs[$header->alg])) {
94
            throw new UnexpectedValueException('Algorithm not supported');
95
        }
96
        if (!in_array($header->alg, $allowed_algs)) {
97
            throw new UnexpectedValueException('Algorithm not allowed');
98
        }
99
        if (is_array($key) || $key instanceof \ArrayAccess) {
100
            if (isset($header->kid)) {
101
                $key = $key[$header->kid];
102
            } else {
103
                throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
104
            }
105
        }
106
107
        // Check the signature
108
        if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
0 ignored issues
show
Bug introduced by
Since verify() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of verify() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
109
            throw new SignatureInvalidException('Signature verification failed');
110
        }
111
112
        // Check if the nbf if it is defined. This is the time that the
113
        // token can actually be used. If it's not yet that time, abort.
114 View Code Duplication
        if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
115
            throw new BeforeValidException(
116
                'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf)
117
            );
118
        }
119
120
        // Check that this token has been created before 'now'. This prevents
121
        // using tokens that have been created for later use (and haven't
122
        // correctly used the nbf claim).
123 View Code Duplication
        if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
124
            throw new BeforeValidException(
125
                'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat)
126
            );
127
        }
128
129
        // Check if this token has expired.
130
        if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
131
            throw new ExpiredException('Expired token');
132
        }
133
134
        return $payload;
135
    }
136
137
    /**
138
     * Converts and signs a PHP object or array into a JWT string.
139
     *
140
     * @param object|array  $payload    PHP object or array
141
     * @param string        $key        The secret key.
142
     *                                  If the algorithm used is asymmetric, this is the private key
143
     * @param string        $alg        The signing algorithm.
144
     *                                  Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
145
     * @param mixed         $keyId
146
     * @param array         $head       An array with header elements to attach
147
     *
148
     * @return string A signed JWT
149
     *
150
     * @uses jsonEncode
151
     * @uses urlsafeB64Encode
152
     */
153
    public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null)
154
    {
155
        $header = array('typ' => 'JWT', 'alg' => $alg);
156
        if ($keyId !== null) {
157
            $header['kid'] = $keyId;
158
        }
159
        if ( isset($head) && is_array($head) ) {
160
            $header = array_merge($head, $header);
161
        }
162
        $segments = array();
163
        $segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
164
        $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
165
        $signing_input = implode('.', $segments);
166
167
        $signature = static::sign($signing_input, $key, $alg);
168
        $segments[] = static::urlsafeB64Encode($signature);
169
170
        return implode('.', $segments);
171
    }
172
173
    /**
174
     * Sign a string with a given key and algorithm.
175
     *
176
     * @param string            $msg    The message to sign
177
     * @param string|resource   $key    The secret key
178
     * @param string            $alg    The signing algorithm.
179
     *                                  Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
180
     *
181
     * @return string An encrypted message
182
     *
183
     * @throws DomainException Unsupported algorithm was specified
184
     */
185
    public static function sign($msg, $key, $alg = 'HS256')
186
    {
187
        if (empty(static::$supported_algs[$alg])) {
188
            throw new DomainException('Algorithm not supported');
189
        }
190
        list($function, $algorithm) = static::$supported_algs[$alg];
191
        switch($function) {
192
            case 'hash_hmac':
193
                return hash_hmac($algorithm, $msg, $key, true);
194 View Code Duplication
            case 'openssl':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
195
                $signature = '';
196
                $success = openssl_sign($msg, $signature, $key, $algorithm);
197
                if (!$success) {
198
                    throw new DomainException("OpenSSL unable to sign data");
199
                } else {
200
                    return $signature;
201
                }
202
        }
203
    }
204
205
    /**
206
     * Verify a signature with the message, key and method. Not all methods
207
     * are symmetric, so we must have a separate verify and sign method.
208
     *
209
     * @param string            $msg        The original message (header and body)
210
     * @param string            $signature  The original signature
211
     * @param string|resource   $key        For HS*, a string key works. for RS*, must be a resource of an openssl public key
212
     * @param string            $alg        The algorithm
213
     *
214
     * @return bool
215
     *
216
     * @throws DomainException Invalid Algorithm or OpenSSL failure
217
     */
218
    private static function verify($msg, $signature, $key, $alg)
219
    {
220
        if (empty(static::$supported_algs[$alg])) {
221
            throw new DomainException('Algorithm not supported');
222
        }
223
224
        list($function, $algorithm) = static::$supported_algs[$alg];
225
        switch($function) {
226 View Code Duplication
            case 'openssl':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
227
                $success = openssl_verify($msg, $signature, $key, $algorithm);
228
                if (!$success) {
229
                    throw new DomainException("OpenSSL unable to verify data: " . openssl_error_string());
230
                } else {
231
                    return $signature;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $signature; (string) is incompatible with the return type documented by Firebase\JWT\JWT::verify of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
232
                }
233
            case 'hash_hmac':
234
            default:
235
                $hash = hash_hmac($algorithm, $msg, $key, true);
236
                if (function_exists('hash_equals')) {
237
                    return hash_equals($signature, $hash);
238
                }
239
                $len = min(static::safeStrlen($signature), static::safeStrlen($hash));
0 ignored issues
show
Bug introduced by
Since safeStrlen() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of safeStrlen() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
240
241
                $status = 0;
242
                for ($i = 0; $i < $len; $i++) {
243
                    $status |= (ord($signature[$i]) ^ ord($hash[$i]));
244
                }
245
                $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash));
0 ignored issues
show
Bug introduced by
Since safeStrlen() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of safeStrlen() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
246
247
                return ($status === 0);
248
        }
249
    }
250
251
    /**
252
     * Decode a JSON string into a PHP object.
253
     *
254
     * @param string $input JSON string
255
     *
256
     * @return object Object representation of JSON string
257
     *
258
     * @throws DomainException Provided string was invalid JSON
259
     */
260
    public static function jsonDecode($input)
261
    {
262
        if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
263
            /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you
264
             * to specify that large ints (like Steam Transaction IDs) should be treated as
265
             * strings, rather than the PHP default behaviour of converting them to floats.
266
             */
267
            $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
268
        } else {
269
            /** Not all servers will support that, however, so for older versions we must
270
             * manually detect large ints in the JSON string and quote them (thus converting
271
             *them to strings) before decoding, hence the preg_replace() call.
272
             */
273
            $max_int_length = strlen((string) PHP_INT_MAX) - 1;
274
            $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input);
275
            $obj = json_decode($json_without_bigints);
276
        }
277
278 View Code Duplication
        if (function_exists('json_last_error') && $errno = json_last_error()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
279
            static::handleJsonError($errno);
0 ignored issues
show
Bug introduced by
Since handleJsonError() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of handleJsonError() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
280
        } elseif ($obj === null && $input !== 'null') {
281
            throw new DomainException('Null result with non-null input');
282
        }
283
        return $obj;
284
    }
285
286
    /**
287
     * Encode a PHP object into a JSON string.
288
     *
289
     * @param object|array $input A PHP object or array
290
     *
291
     * @return string JSON representation of the PHP object or array
292
     *
293
     * @throws DomainException Provided object could not be encoded to valid JSON
294
     */
295
    public static function jsonEncode($input)
296
    {
297
        $json = json_encode($input);
298 View Code Duplication
        if (function_exists('json_last_error') && $errno = json_last_error()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
299
            static::handleJsonError($errno);
0 ignored issues
show
Bug introduced by
Since handleJsonError() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of handleJsonError() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
300
        } elseif ($json === 'null' && $input !== null) {
301
            throw new DomainException('Null result with non-null input');
302
        }
303
        return $json;
304
    }
305
306
    /**
307
     * Decode a string with URL-safe Base64.
308
     *
309
     * @param string $input A Base64 encoded string
310
     *
311
     * @return string A decoded string
312
     */
313
    public static function urlsafeB64Decode($input)
314
    {
315
        $remainder = strlen($input) % 4;
316
        if ($remainder) {
317
            $padlen = 4 - $remainder;
318
            $input .= str_repeat('=', $padlen);
319
        }
320
        return base64_decode(strtr($input, '-_', '+/'));
321
    }
322
323
    /**
324
     * Encode a string with URL-safe Base64.
325
     *
326
     * @param string $input The string you want encoded
327
     *
328
     * @return string The base64 encode of what you passed in
329
     */
330
    public static function urlsafeB64Encode($input)
331
    {
332
        return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
333
    }
334
335
    /**
336
     * Helper method to create a JSON error.
337
     *
338
     * @param int $errno An error number from json_last_error()
339
     *
340
     * @return void
341
     */
342
    private static function handleJsonError($errno)
343
    {
344
        $messages = array(
345
            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
346
            JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
347
            JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON'
348
        );
349
        throw new DomainException(
350
            isset($messages[$errno])
351
            ? $messages[$errno]
352
            : 'Unknown JSON error: ' . $errno
353
        );
354
    }
355
356
    /**
357
     * Get the number of bytes in cryptographic strings.
358
     *
359
     * @param string
360
     *
361
     * @return int
362
     */
363
    private static function safeStrlen($str)
364
    {
365
        if (function_exists('mb_strlen')) {
366
            return mb_strlen($str, '8bit');
367
        }
368
        return strlen($str);
369
    }
370
}
371