Jwt   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 362
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 47
lcom 1
cbo 0
dl 0
loc 362
rs 8.439
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A setLeeway() 0 5 1
A setTimestamp() 0 5 1
A setAlg() 0 5 1
A getAlgs() 0 4 1
C decode() 0 66 18
A encode() 0 23 3
A sign() 0 14 3
A signHmac() 0 10 2
A verify() 0 15 3
A verifyHmac() 0 10 2
A hashEquals() 0 19 4
A jsonDecode() 0 10 2
A jsonEncode() 0 10 2
A decodeBase64() 0 11 2
A encodeBase64() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Jwt often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jwt, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @package Oauth
5
 * @author Iurii Makukh <[email protected]>
6
 * @copyright Copyright (c) 2017, Iurii Makukh
7
 * @license https://www.gnu.org/licenses/gpl.html GNU/GPLv3
8
 */
9
10
namespace gplcart\modules\oauth\helpers;
11
12
use DateTime;
13
use InvalidArgumentException;
14
use LogicException;
15
use OutOfRangeException;
16
use RuntimeException;
17
use UnexpectedValueException;
18
19
/**
20
 * JSON Web Token implementation
21
 */
22
class Jwt
23
{
24
25
    /**
26
     * Extra leeway time when checking nbf, iat or expiration times
27
     */
28
    protected $leeway = 0;
29
30
    /**
31
     * The current time
32
     */
33
    protected $timestamp;
34
35
    /**
36
     * An array of supported algorithms
37
     * @var array
38
     */
39
    protected $algs;
40
41
    /**
42
     * Jwt constructor
43
     */
44
    public function __construct()
45
    {
46
        $this->algs = array(
47
            'HS256' => array('SHA256', array($this, 'signHmac'), array($this, 'verifyHmac')),
48
            'HS512' => array('SHA512', array($this, 'signHmac'), array($this, 'verifyHmac'))
49
        );
50
    }
51
52
    /**
53
     * Sets leeway time
54
     * @param int $time
55
     * @return $this
56
     */
57
    public function setLeeway($time)
58
    {
59
        $this->leeway = $time;
60
        return $this;
61
    }
62
63
    /**
64
     * Sets the current timestamp
65
     * @param $timestamp
66
     * @return $this
67
     */
68
    public function setTimestamp($timestamp)
69
    {
70
        $this->timestamp = $timestamp;
71
        return $this;
72
    }
73
74
    /**
75
     * Sets an algorithm
76
     * @param string $name
77
     * @param string $hash_method
78
     * @param callable $signer
79
     * @param callable $verifier
80
     * @return $this
81
     */
82
    public function setAlg($name, $hash_method, callable $signer, callable $verifier)
83
    {
84
        $this->algs[strtoupper($name)] = array(strtoupper($hash_method), $signer, $verifier);
85
        return $this;
86
    }
87
88
    /**
89
     * Returns an array of supported algorithms
90
     * @return array
91
     */
92
    public function getAlgs()
93
    {
94
        return $this->algs;
95
    }
96
97
    /**
98
     * Decodes a JWT string into a PHP object
99
     * @param string $jwt
100
     * @param string $key
101
     * @param array $allowed_algs
102
     * @return object
103
     * @throws RuntimeException
104
     * @throws InvalidArgumentException
105
     * @throws UnexpectedValueException
106
     */
107
    public function decode($jwt, $key, array $allowed_algs = array())
108
    {
109
        if (!isset($this->timestamp)) {
110
            $this->timestamp = time();
111
        }
112
113
        if (empty($key)) {
114
            throw new InvalidArgumentException('Key may not be empty');
115
        }
116
117
        $tks = explode('.', $jwt);
118
119
        if (count($tks) != 3) {
120
            throw new UnexpectedValueException('Wrong number of segments');
121
        }
122
123
        list($headb64, $bodyb64, $cryptob64) = $tks;
124
125
        $header = $this->jsonDecode($this->decodeBase64($headb64));
126
127
        if (!isset($header)) {
128
            throw new UnexpectedValueException('Invalid header encoding');
129
        }
130
131
        $payload = $this->jsonDecode($this->decodeBase64($bodyb64));
132
133
        if (!isset($payload)) {
134
            throw new UnexpectedValueException('Invalid claims encoding');
135
        }
136
137
        $sig = $this->decodeBase64($cryptob64);
138
139
        if ($sig === false) {
140
            throw new UnexpectedValueException('Invalid signature encoding');
141
        }
142
143
        if (empty($header->alg)) {
144
            throw new UnexpectedValueException('Empty algorithm');
145
        }
146
147
        if (empty($this->algs[$header->alg])) {
148
            throw new UnexpectedValueException('Algorithm not supported');
149
        }
150
151
        if (!empty($allowed_algs) && !in_array($header->alg, $allowed_algs)) {
152
            throw new RuntimeException('Algorithm not allowed');
153
        }
154
155
        if (!$this->verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
156
            throw new RuntimeException('Signature verification failed');
157
        }
158
159
        if (isset($payload->nbf) && $payload->nbf > ($this->timestamp + $this->leeway)) {
160
            throw new RuntimeException('Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf));
161
        }
162
163
        if (isset($payload->iat) && $payload->iat > ($this->timestamp + $this->leeway)) {
164
            throw new RuntimeException('Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat));
165
        }
166
167
        if (isset($payload->exp) && ($this->timestamp - $this->leeway) >= $payload->exp) {
168
            throw new RuntimeException('Expired token');
169
        }
170
171
        return $payload;
172
    }
173
174
    /**
175
     * Converts and signs a PHP object or array into a JWT string
176
     * @param object|array $payload
177
     * @param string $key
178
     * @param string $alg
179
     * @param null|string $key_id
180
     * @param array $head
181
     * @return string
182
     */
183
    public function encode($payload, $key, $alg = 'HS256', $key_id = null, array $head = array())
184
    {
185
        $header = array('typ' => 'JWT', 'alg' => $alg);
186
187
        if (isset($key_id)) {
188
            $header['kid'] = $key_id;
189
        }
190
191
        if (!empty($head)) {
192
            $header = array_merge($head, $header);
193
        }
194
195
        $segments = array();
196
        $segments[] = $this->encodeBase64($this->jsonEncode($header));
197
        $segments[] = $this->encodeBase64($this->jsonEncode($payload));
198
199
        $signing_input = implode('.', $segments);
200
        $signature = $this->sign($signing_input, $key, $alg);
201
202
        $segments[] = $this->encodeBase64($signature);
203
204
        return implode('.', $segments);
205
    }
206
207
    /**
208
     * Sign a string with a given key and algorithm
209
     * @param string $data
210
     * @param string|resource $key
211
     * @param string $alg
212
     * @return string
213
     * @throws OutOfRangeException
214
     * @throws RuntimeException
215
     * @throws LogicException
216
     */
217
    public function sign($data, $key, $alg = 'HS256')
218
    {
219
        if (empty($this->algs[$alg])) {
220
            throw new OutOfRangeException('Algorithm not supported');
221
        }
222
223
        list($func_alg, $function) = $this->algs[$alg];
224
225
        if (is_callable($function)) {
226
            return $function($data, $key, $func_alg);
227
        }
228
229
        throw new LogicException('Unknown signer');
230
    }
231
232
    /**
233
     * Generate signature using HMAC method
234
     * @param string $data
235
     * @param string $key
236
     * @param string $alg
237
     * @return string
238
     * @throws RuntimeException
239
     */
240
    protected function signHmac($data, $key, $alg)
241
    {
242
        $hash = hash_hmac($alg, $data, $key, true);
243
244
        if (empty($hash)) {
245
            throw new RuntimeException('Unable to sign data using HMAC method');
246
        }
247
248
        return $hash;
249
    }
250
251
    /**
252
     * Verify a signature with the message, key and method
253
     * @param string $data
254
     * @param string $hash
255
     * @param string|resource $key
256
     * @param string $alg
257
     * @return bool
258
     * @throws OutOfRangeException
259
     * @throws LogicException
260
     */
261
    public function verify($data, $hash, $key, $alg)
262
    {
263
        if (empty($this->algs[$alg])) {
264
            throw new OutOfRangeException('Algorithm not supported');
265
        }
266
267
        $func_alg = $this->algs[$alg][0];
268
        $function = $this->algs[$alg][2];
269
270
        if (is_callable($function)) {
271
            return $function($data, $hash, $key, $func_alg);
272
        }
273
274
        throw new LogicException('Unsupported verifier');
275
    }
276
277
    /**
278
     * Verify signature using HMAC method
279
     * @param string $data
280
     * @param string $hash
281
     * @param string $key
282
     * @param string $alg
283
     * @return bool
284
     * @throws RuntimeException
285
     */
286
    protected function verifyHmac($data, $hash, $key, $alg)
287
    {
288
        $hashed = hash_hmac($alg, $data, $key, true);
289
290
        if (empty($hashed)) {
291
            throw new RuntimeException('Unable to hash data for verifying using HMAC method');
292
        }
293
294
        return $this->hashEquals($hash, $hashed);
295
    }
296
297
    /**
298
     * Compares two hashed strings
299
     * @param string $str1
300
     * @param string $str2
301
     * @return boolean
302
     */
303
    protected function hashEquals($str1, $str2)
304
    {
305
        if (function_exists('hash_equals')) {
306
            return hash_equals($str1, $str2);
307
        }
308
309
        if (strlen($str1) != strlen($str2)) {
310
            return false;
311
        }
312
313
        $res = $str1 ^ $str2;
314
        $ret = 0;
315
316
        for ($i = strlen($res) - 1; $i >= 0; $i--) {
317
            $ret |= ord($res[$i]);
318
        }
319
320
        return !$ret;
321
    }
322
323
    /**
324
     * Decode a JSON string into a PHP object
325
     * @param string $input
326
     * @return object
327
     * @throws RuntimeException
328
     */
329
    protected function jsonDecode($input)
330
    {
331
        $object = json_decode($input);
332
333
        if (json_last_error() === JSON_ERROR_NONE) {
334
            return $object;
335
        }
336
337
        throw new RuntimeException('Failed to decode JSON string');
338
    }
339
340
    /**
341
     * Encode a PHP object into a JSON string
342
     * @param object|array $input
343
     * @return string
344
     * @throws RuntimeException
345
     */
346
    protected function jsonEncode($input)
347
    {
348
        $json = json_encode($input);
349
350
        if (json_last_error() === JSON_ERROR_NONE) {
351
            return $json;
352
        }
353
354
        throw new RuntimeException('Failed to encode JSON string');
355
    }
356
357
    /**
358
     * Decodes data encoded with MIME base64
359
     * @param string $string
360
     * @return string
361
     */
362
    protected function decodeBase64($string)
363
    {
364
        $remainder = strlen($string) % 4;
365
366
        if ($remainder) {
367
            $padlen = 4 - $remainder;
368
            $string .= str_repeat('=', $padlen);
369
        }
370
371
        return base64_decode(strtr($string, '-_', '+/'));
372
    }
373
374
    /**
375
     * Safe URL base64 encoding
376
     * @param string $string
377
     * @return string
378
     */
379
    protected function encodeBase64($string)
380
    {
381
        return str_replace('=', '', strtr(base64_encode($string), '+/', '-_'));
382
    }
383
}