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.

JWT   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 438
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 47
eloc 107
dl 0
loc 438
ccs 123
cts 123
cp 1
rs 8.64
c 0
b 0
f 0

23 Methods

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