GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Branch php72 (880eb0)
by Joni
05:58
created

JWT   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 441
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 41
eloc 95
dl 0
loc 441
ccs 107
cts 107
cp 1
rs 9.1199
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A unsecuredFromClaims() 0 4 1
A signedFromClaims() 0 6 1
A claims() 0 16 3
A __construct() 0 12 3
A isUnsecured() 0 8 2
A _validatedPayloadFromJWS() 0 8 2
A _validatedPayloadFromUnsecuredJWS() 0 10 3
A isNested() 0 11 3
A _validatedPayloadFromJWE() 0 12 3
A _validatedPayloadFromSignedJWS() 0 16 4
A __toString() 0 3 1
A JWE() 0 6 2
A JWS() 0 6 2
A token() 0 3 1
A header() 0 4 1
A encryptedFromClaims() 0 7 1
A _claimsFromNestedPayload() 0 9 2
A isJWE() 0 3 1
A isJWS() 0 3 1
A encryptNested() 0 13 2
A signNested() 0 10 2

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.

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
declare(strict_types = 1);
4
5
namespace Sop\JWX\JWT;
6
7
use Sop\JWX\JWE\CompressionAlgorithm;
8
use Sop\JWX\JWE\ContentEncryptionAlgorithm;
9
use Sop\JWX\JWE\JWE;
10
use Sop\JWX\JWE\KeyManagementAlgorithm;
11
use Sop\JWX\JWK\JWKSet;
12
use Sop\JWX\JWS\Algorithm\NoneAlgorithm;
13
use Sop\JWX\JWS\JWS;
14
use Sop\JWX\JWS\SignatureAlgorithm;
15
use Sop\JWX\JWT\Exception\ValidationException;
16
use Sop\JWX\JWT\Header\Header;
17
use Sop\JWX\JWT\Header\JOSE;
18
use Sop\JWX\JWT\Parameter\ContentTypeParameter;
19
use Sop\JWX\Util\Base64;
20
21
/**
22
 * Represents a token as a JWS or a JWE compact serialization with claims
23
 * as a payload.
24
 *
25
 * @see https://tools.ietf.org/html/rfc7519#section-3
26
 */
27
class JWT
28
{
29
    /**
30
     * Type identifier for the signed JWT.
31
     *
32
     * @internal
33
     *
34
     * @var int
35
     */
36
    const TYPE_JWS = 0;
37
38
    /**
39
     * Type identifier for the encrypted JWT.
40
     *
41
     * @internal
42
     *
43
     * @var int
44
     */
45
    const TYPE_JWE = 1;
46
47
    /**
48
     * JWT parts.
49
     *
50
     * @var string[]
51
     */
52
    protected $_parts;
53
54
    /**
55
     * JWT type.
56
     *
57
     * @var int
58
     */
59
    protected $_type;
60
61
    /**
62
     * Constructor.
63
     *
64
     * @param string $token JWT string
65
     *
66
     * @throws \UnexpectedValueException
67
     */
68 14
    public function __construct(string $token)
69
    {
70 14
        $this->_parts = explode('.', $token);
71 14
        switch (count($this->_parts)) {
72 14
            case 3:
73 9
                $this->_type = self::TYPE_JWS;
74 9
                break;
75 5
            case 5:
76 4
                $this->_type = self::TYPE_JWE;
77 4
                break;
78
            default:
79 1
                throw new \UnexpectedValueException('Not a JWT token.');
80
        }
81 13
    }
82
83
    /**
84
     * Convert JWT to string.
85
     *
86
     * @return string
87
     */
88 1
    public function __toString(): string
89
    {
90 1
        return $this->token();
91
    }
92
93
    /**
94
     * Convert claims set to an unsecured JWT.
95
     *
96
     * Unsecured JWT is not signed nor encrypted neither integrity protected,
97
     * and should thus be handled with care!
98
     *
99
     * @see https://tools.ietf.org/html/rfc7519#section-6
100
     *
101
     * @param Claims      $claims Claims set
102
     * @param null|Header $header Optional header
103
     *
104
     * @throws \RuntimeException For generic errors
105
     *
106
     * @return self
107
     */
108 3
    public static function unsecuredFromClaims(Claims $claims,
109
        ?Header $header = null): self
110
    {
111 3
        return self::signedFromClaims($claims, new NoneAlgorithm(), $header);
112
    }
113
114
    /**
115
     * Convert claims set to a signed JWS token.
116
     *
117
     * @param Claims             $claims Claims set
118
     * @param SignatureAlgorithm $algo   Signature algorithm
119
     * @param null|Header        $header Optional header
120
     *
121
     * @throws \RuntimeException For generic errors
122
     *
123
     * @return self
124
     */
125 5
    public static function signedFromClaims(Claims $claims,
126
        SignatureAlgorithm $algo, ?Header $header = null): self
127
    {
128 5
        $payload = $claims->toJSON();
129 5
        $jws = JWS::sign($payload, $algo, $header);
130 5
        return new self($jws->toCompact());
131
    }
132
133
    /**
134
     * Convert claims set to an encrypted JWE token.
135
     *
136
     * @param Claims                     $claims   Claims set
137
     * @param KeyManagementAlgorithm     $key_algo Key management algorithm
138
     * @param ContentEncryptionAlgorithm $enc_algo Content encryption algorithm
139
     * @param null|CompressionAlgorithm  $zip_algo Optional compression algorithm
140
     * @param null|Header                $header   Optional header
141
     *
142
     * @throws \RuntimeException For generic errors
143
     *
144
     * @return self
145
     */
146 2
    public static function encryptedFromClaims(Claims $claims,
147
        KeyManagementAlgorithm $key_algo, ContentEncryptionAlgorithm $enc_algo,
148
        ?CompressionAlgorithm $zip_algo = null, ?Header $header = null): self
149
    {
150 2
        $payload = $claims->toJSON();
151 2
        $jwe = JWE::encrypt($payload, $key_algo, $enc_algo, $zip_algo, $header);
152 2
        return new self($jwe->toCompact());
153
    }
154
155
    /**
156
     * Get claims from the JWT.
157
     *
158
     * Claims shall be validated according to given validation context.
159
     * Validation context must contain all the necessary keys for the signature
160
     * validation and/or content decryption.
161
     *
162
     * If validation context contains only one key, it shall be used explicitly.
163
     * If multiple keys are provided, they must contain a JWK ID parameter for
164
     * the key identification.
165
     *
166
     * @param ValidationContext $ctx
167
     *
168
     * @throws ValidationException if signature is invalid, or decryption fails,
169
     *                             or claims validation fails
170
     * @throws \RuntimeException   For generic errors
171
     *
172
     * @return Claims
173
     */
174 11
    public function claims(ValidationContext $ctx): Claims
175
    {
176
        // check signature or decrypt depending on the JWT type.
177 11
        if ($this->isJWS()) {
178 8
            $payload = self::_validatedPayloadFromJWS($this->JWS(), $ctx);
179
        } else {
180 4
            $payload = self::_validatedPayloadFromJWE($this->JWE(), $ctx);
181
        }
182
        // if JWT contains a nested token
183 6
        if ($this->isNested()) {
184 1
            return $this->_claimsFromNestedPayload($payload, $ctx);
185
        }
186
        // decode claims and validate
187 6
        $claims = Claims::fromJSON($payload);
188 6
        $ctx->validate($claims);
189 6
        return $claims;
190
    }
191
192
    /**
193
     * Sign self producing a nested JWT.
194
     *
195
     * Note that if JWT is to be signed and encrypted, it should be done in
196
     * sign-then-encrypt order. Please refer to links for security information.
197
     *
198
     * @see https://tools.ietf.org/html/rfc7519#section-11.2
199
     *
200
     * @param SignatureAlgorithm $algo   Signature algorithm
201
     * @param null|Header        $header Optional header
202
     *
203
     * @throws \RuntimeException For generic errors
204
     *
205
     * @return self
206
     */
207 1
    public function signNested(SignatureAlgorithm $algo, ?Header $header = null): self
208
    {
209 1
        if (!isset($header)) {
210 1
            $header = new Header();
211
        }
212
        // add JWT content type parameter
213 1
        $header = $header->withParameters(
214 1
            new ContentTypeParameter(ContentTypeParameter::TYPE_JWT));
215 1
        $jws = JWS::sign($this->token(), $algo, $header);
216 1
        return new self($jws->toCompact());
217
    }
218
219
    /**
220
     * Encrypt self producing a nested JWT.
221
     *
222
     * This JWT should be a JWS, that is, the order of nesting should be
223
     * sign-then-encrypt.
224
     *
225
     * @see https://tools.ietf.org/html/rfc7519#section-11.2
226
     *
227
     * @param KeyManagementAlgorithm     $key_algo Key management algorithm
228
     * @param ContentEncryptionAlgorithm $enc_algo Content encryption algorithm
229
     * @param null|CompressionAlgorithm  $zip_algo Optional compression algorithm
230
     * @param null|Header                $header   Optional header
231
     *
232
     * @throws \RuntimeException For generic errors
233
     *
234
     * @return self
235
     */
236 1
    public function encryptNested(KeyManagementAlgorithm $key_algo,
237
        ContentEncryptionAlgorithm $enc_algo,
238
        ?CompressionAlgorithm $zip_algo = null, ?Header $header = null): self
239
    {
240 1
        if (!isset($header)) {
241 1
            $header = new Header();
242
        }
243
        // add JWT content type parameter
244 1
        $header = $header->withParameters(
245 1
            new ContentTypeParameter(ContentTypeParameter::TYPE_JWT));
246 1
        $jwe = JWE::encrypt($this->token(), $key_algo, $enc_algo, $zip_algo,
247 1
            $header);
248 1
        return new self($jwe->toCompact());
249
    }
250
251
    /**
252
     * Whether JWT is a JWS.
253
     *
254
     * @return bool
255
     */
256 16
    public function isJWS(): bool
257
    {
258 16
        return self::TYPE_JWS === $this->_type;
259
    }
260
261
    /**
262
     * Get JWT as a JWS.
263
     *
264
     * @throws \LogicException
265
     *
266
     * @return JWS
267
     */
268 12
    public function JWS(): JWS
269
    {
270 12
        if (!$this->isJWS()) {
271 1
            throw new \LogicException('Not a JWS.');
272
        }
273 11
        return JWS::fromParts($this->_parts);
274
    }
275
276
    /**
277
     * Whether JWT is a JWE.
278
     *
279
     * @return bool
280
     */
281 11
    public function isJWE(): bool
282
    {
283 11
        return self::TYPE_JWE === $this->_type;
284
    }
285
286
    /**
287
     * Get JWT as a JWE.
288
     *
289
     * @throws \LogicException
290
     *
291
     * @return JWE
292
     */
293 7
    public function JWE(): JWE
294
    {
295 7
        if (!$this->isJWE()) {
296 1
            throw new \LogicException('Not a JWE.');
297
        }
298 6
        return JWE::fromParts($this->_parts);
299
    }
300
301
    /**
302
     * Check whether JWT contains another nested JWT.
303
     *
304
     * @return bool
305
     */
306 9
    public function isNested(): bool
307
    {
308 9
        $header = $this->header();
309 9
        if (!$header->hasContentType()) {
310 7
            return false;
311
        }
312 3
        $cty = $header->contentType()->value();
313 3
        if (ContentTypeParameter::TYPE_JWT !== $cty) {
314 1
            return false;
315
        }
316 2
        return true;
317
    }
318
319
    /**
320
     * Check whether JWT is unsecured, that is, it's neither integrity protected
321
     * nor encrypted.
322
     *
323
     * @return bool
324
     */
325 4
    public function isUnsecured(): bool
326
    {
327
        // encrypted JWT shall be considered secure
328 4
        if ($this->isJWE()) {
329 2
            return false;
330
        }
331
        // check whether JWS is unsecured
332 2
        return $this->JWS()->isUnsecured();
333
    }
334
335
    /**
336
     * Get JWT header.
337
     *
338
     * @return JOSE
339
     */
340 11
    public function header(): JOSE
341
    {
342 11
        $header = Header::fromJSON(Base64::urlDecode($this->_parts[0]));
343 11
        return new JOSE($header);
344
    }
345
346
    /**
347
     * Get JWT as a string.
348
     *
349
     * @return string
350
     */
351 6
    public function token(): string
352
    {
353 6
        return implode('.', $this->_parts);
354
    }
355
356
    /**
357
     * Get claims from a nested payload.
358
     *
359
     * @param string            $payload JWT payload
360
     * @param ValidationContext $ctx     Validation context
361
     *
362
     * @return Claims
363
     */
364 1
    private function _claimsFromNestedPayload(string $payload,
365
        ValidationContext $ctx): Claims
366
    {
367 1
        $jwt = new JWT($payload);
368
        // if this token secured, allow nested tokens to be unsecured.
369 1
        if (!$this->isUnsecured()) {
370 1
            $ctx = $ctx->withUnsecuredAllowed(true);
371
        }
372 1
        return $jwt->claims($ctx);
373
    }
374
375
    /**
376
     * Get validated payload from JWS.
377
     *
378
     * @param JWS               $jws JWS
379
     * @param ValidationContext $ctx Validation context
380
     *
381
     * @throws ValidationException If signature validation fails
382
     *
383
     * @return string
384
     */
385 8
    private static function _validatedPayloadFromJWS(JWS $jws,
386
        ValidationContext $ctx): string
387
    {
388
        // if JWS is unsecured
389 8
        if ($jws->isUnsecured()) {
390 3
            return self::_validatedPayloadFromUnsecuredJWS($jws, $ctx);
391
        }
392 5
        return self::_validatedPayloadFromSignedJWS($jws, $ctx->keys());
393
    }
394
395
    /**
396
     * Get validated payload from an unsecured JWS.
397
     *
398
     * @param JWS               $jws JWS
399
     * @param ValidationContext $ctx Validation context
400
     *
401
     * @throws ValidationException If unsecured JWT's are not allowed, or JWS
402
     *                             token is malformed
403
     *
404
     * @return string
405
     */
406 3
    private static function _validatedPayloadFromUnsecuredJWS(JWS $jws,
407
        ValidationContext $ctx): string
408
    {
409 3
        if (!$ctx->isUnsecuredAllowed()) {
410 1
            throw new ValidationException('Unsecured JWS not allowed.');
411
        }
412 2
        if (!$jws->validate(new NoneAlgorithm())) {
413 1
            throw new ValidationException('Malformed unsecured token.');
414
        }
415 1
        return $jws->payload();
416
    }
417
418
    /**
419
     * Get validated payload from a signed JWS.
420
     *
421
     * @param JWS    $jws  JWS
422
     * @param JWKSet $keys Set of allowed keys for the signature validation
423
     *
424
     * @throws ValidationException If validation fails
425
     *
426
     * @return string
427
     */
428 5
    private static function _validatedPayloadFromSignedJWS(JWS $jws, JWKSet $keys): string
429
    {
430
        try {
431
            // explicitly defined key
432 5
            if (1 === count($keys)) {
433 2
                $valid = $jws->validateWithJWK($keys->first());
434
            } else {
435 5
                $valid = $jws->validateWithJWKSet($keys);
436
            }
437 1
        } catch (\RuntimeException $e) {
438 1
            throw new ValidationException('JWS validation failed.', 0, $e);
439
        }
440 4
        if (!$valid) {
441 1
            throw new ValidationException('JWS signature is invalid.');
442
        }
443 3
        return $jws->payload();
444
    }
445
446
    /**
447
     * Get validated payload from an encrypted JWE.
448
     *
449
     * @param JWE               $jwe JWE
450
     * @param ValidationContext $ctx Validation context
451
     *
452
     * @throws ValidationException If decryption fails
453
     *
454
     * @return string
455
     */
456 4
    private static function _validatedPayloadFromJWE(JWE $jwe,
457
        ValidationContext $ctx): string
458
    {
459
        try {
460 4
            $keys = $ctx->keys();
461
            // explicitly defined key
462 4
            if (1 === count($keys)) {
463 1
                return $jwe->decryptWithJWK($keys->first());
464
            }
465 3
            return $jwe->decryptWithJWKSet($keys);
466 1
        } catch (\RuntimeException $e) {
467 1
            throw new ValidationException('JWE validation failed.', 0, $e);
468
        }
469
    }
470
}
471