Failed Conditions
Push — master ( 0699e7...dc4823 )
by Florent
06:51
created

IdTokenBuilder   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 471
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 19

Importance

Changes 0
Metric Value
wmc 63
lcom 1
cbo 19
dl 0
loc 471
rs 3.36
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
B setAccessToken() 0 22 7
A withAccessTokenId() 0 4 1
A withAuthorizationCodeId() 0 4 1
A withClaimsLocales() 0 4 1
A withAuthenticationTime() 0 4 1
A withScope() 0 4 1
A withRequestedClaims() 0 4 1
A withNonce() 0 4 1
A withExpirationAt() 0 4 1
A withoutAuthenticationTime() 0 4 1
A withSignature() 0 12 3
A withEncryption() 0 12 3
A build() 0 24 5
A updateClaimsWithJwtClaims() 0 15 2
A updateClaimsWithAuthenticationTime() 0 8 4
A updateClaimsWithNonce() 0 8 2
A updateClaimsAudience() 0 10 1
A computeIdToken() 0 15 1
A tryToEncrypt() 0 25 2
A getSignatureKey() 0 22 4
A getHeaders() 0 12 2
A updateClaimsWithTokenHash() 0 14 4
A getHash() 0 4 1
A getHashMethod() 0 23 2
A getHashSize() 0 23 2
B getClientKeySet() 0 30 8

How to fix   Complexity   

Complex Class

Complex classes like IdTokenBuilder 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 IdTokenBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2018 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace OAuth2Framework\Component\OpenIdConnect;
15
16
use Base64Url\Base64Url;
17
use Jose\Component\Core\Converter\StandardConverter;
18
use Jose\Component\Core\JWK;
19
use Jose\Component\Core\JWKSet;
20
use Jose\Component\Encryption\JWEBuilder;
21
use Jose\Component\Encryption\Serializer\CompactSerializer as JweCompactSerializer;
22
use Jose\Component\KeyManagement\JKUFactory;
23
use Jose\Component\Signature\JWSBuilder;
24
use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer;
25
use OAuth2Framework\Component\AuthorizationCodeGrant\AuthorizationCodeId;
26
use OAuth2Framework\Component\AuthorizationCodeGrant\AuthorizationCodeRepository;
27
use OAuth2Framework\Component\Core\AccessToken\AccessToken;
28
use OAuth2Framework\Component\Core\AccessToken\AccessTokenId;
29
use OAuth2Framework\Component\Core\Client\Client;
30
use OAuth2Framework\Component\Core\Token\TokenId;
31
use OAuth2Framework\Component\Core\UserAccount\UserAccount;
32
use OAuth2Framework\Component\OpenIdConnect\UserInfo\UserInfo;
33
34
class IdTokenBuilder
35
{
36
    /**
37
     * @var string
38
     */
39
    private $issuer;
40
41
    /**
42
     * @var Client
43
     */
44
    private $client;
45
46
    /**
47
     * @var
48
     */
49
    private $user;
50
51
    /**
52
     * @var UserAccount
53
     */
54
    private $userAccount;
55
56
    /**
57
     * @var string
58
     */
59
    private $redirectUri;
60
61
    /**
62
     * @var UserInfo
63
     */
64
    private $userinfo;
65
66
    /**
67
     * @var
68
     */
69
    private $signatureKeys;
70
71
    /**
72
     * @var int
73
     */
74
    private $lifetime;
75
76
    /**
77
     * @var
78
     */
79
    private $scope;
80
81
    /**
82
     * @var array
83
     */
84
    private $requestedClaims = [];
85
86
    /**
87
     * @var
88
     */
89
    private $claimsLocales;
90
91
    /**
92
     * @var
93
     */
94
    private $accessTokenId;
95
96
    /**
97
     * @var
98
     */
99
    private $authorizationCodeId;
100
101
    /**
102
     * @var
103
     */
104
    private $nonce;
105
106
    /**
107
     * @var bool
108
     */
109
    private $withAuthenticationTime = false;
110
111
    /**
112
     * @var
113
     */
114
    private $jwsBuilder;
115
116
    /**
117
     * @var
118
     */
119
    private $signatureAlgorithm;
120
121
    /**
122
     * @var
123
     */
124
    private $jweBuilder;
125
126
    /**
127
     * @var
128
     */
129
    private $keyEncryptionAlgorithm;
130
131
    /**
132
     * @var
133
     */
134
    private $contentEncryptionAlgorithm;
135
136
    /**
137
     * @var
138
     */
139
    private $expiresAt;
140
141
    /**
142
     * @var JKUFactory|null
143
     */
144
    private $jkuFactory;
145
146
    /**
147
     * @var AuthorizationCodeRepository|null
148
     */
149
    private $authorizationCodeRepository;
150
151
    public function __construct(string $issuer, UserInfo $userinfo, int $lifetime, Client $client, UserAccount $userAccount, string $redirectUri, ?JKUFactory $jkuFactory, ?AuthorizationCodeRepository $authorizationCodeRepository)
152
    {
153
        $this->issuer = $issuer;
154
        $this->userinfo = $userinfo;
155
        $this->lifetime = $lifetime;
156
        $this->client = $client;
157
        $this->user = $user;
0 ignored issues
show
Bug introduced by
The variable $user does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
158
        $this->userAccount = $userAccount;
159
        $this->redirectUri = $redirectUri;
160
        $this->jkuFactory = $jkuFactory;
161
        $this->authorizationCodeRepository = $authorizationCodeRepository;
162
    }
163
164
    public function setAccessToken(AccessToken $accessToken): void
165
    {
166
        $this->accessTokenId = $accessToken->getTokenId();
167
        $this->expiresAt = $accessToken->getExpiresAt();
168
        $this->scope = $accessToken->getParameter()->has('scope') ? $accessToken->getParameter()->get('scope') : null;
169
170
        if ($accessToken->getMetadata()->has('authorization_code_id') && null !== $this->authorizationCodeRepository) {
171
            $authorizationCodeId = new AuthorizationCodeId($accessToken->getMetadata()->get('authorization_code_id'));
172
            $authorizationCode = $this->authorizationCodeRepository->find($authorizationCodeId);
173
            if (null === $authorizationCode) {
174
                return;
175
            }
176
            $this->authorizationCodeId = $authorizationCodeId;
177
            $queryParams = $authorizationCode->getQueryParams();
178
            foreach (['nonce' => 'nonce', 'claims_locales' => 'claimsLocales'] as $k => $v) {
179
                if (\array_key_exists($k, $queryParams)) {
180
                    $this->$v = $queryParams[$k];
181
                }
182
            }
183
            $this->withAuthenticationTime = \array_key_exists('max_age', $authorizationCode->getQueryParams());
184
        }
185
    }
186
187
    public function withAccessTokenId(AccessTokenId $accessTokenId): void
188
    {
189
        $this->accessTokenId = $accessTokenId;
190
    }
191
192
    public function withAuthorizationCodeId(AuthorizationCodeId $authorizationCodeId): void
193
    {
194
        $this->authorizationCodeId = $authorizationCodeId;
195
    }
196
197
    public function withClaimsLocales(string $claimsLocales): void
198
    {
199
        $this->claimsLocales = $claimsLocales;
200
    }
201
202
    public function withAuthenticationTime(): void
203
    {
204
        $this->withAuthenticationTime = true;
205
    }
206
207
    public function withScope(string $scope): void
208
    {
209
        $this->scope = $scope;
210
    }
211
212
    public function withRequestedClaims(array $requestedClaims): void
213
    {
214
        $this->requestedClaims = $requestedClaims;
215
    }
216
217
    public function withNonce(string $nonce): void
218
    {
219
        $this->nonce = $nonce;
220
    }
221
222
    public function withExpirationAt(\DateTimeImmutable $expiresAt): void
223
    {
224
        $this->expiresAt = $expiresAt;
225
    }
226
227
    public function withoutAuthenticationTime(): void
228
    {
229
        $this->withAuthenticationTime = false;
230
    }
231
232
    public function withSignature(JWSBuilder $jwsBuilder, JWKSet $signatureKeys, string $signatureAlgorithm): void
233
    {
234
        if (!\in_array($signatureAlgorithm, $jwsBuilder->getSignatureAlgorithmManager()->list(), true)) {
235
            throw new \InvalidArgumentException(\Safe\sprintf('Unsupported signature algorithm "%s". Please use one of the following one: %s', $signatureAlgorithm, \implode(', ', $jwsBuilder->getSignatureAlgorithmManager()->list())));
236
        }
237
        if (0 === $signatureKeys->count()) {
238
            throw new \InvalidArgumentException('The signature key set must contain at least one key.');
239
        }
240
        $this->jwsBuilder = $jwsBuilder;
241
        $this->signatureKeys = $signatureKeys;
242
        $this->signatureAlgorithm = $signatureAlgorithm;
243
    }
244
245
    public function withEncryption(JWEBuilder $jweBuilder, string $keyEncryptionAlgorithm, string $contentEncryptionAlgorithm): void
246
    {
247
        if (!\in_array($keyEncryptionAlgorithm, $jweBuilder->getKeyEncryptionAlgorithmManager()->list(), true)) {
248
            throw new \InvalidArgumentException(\Safe\sprintf('Unsupported key encryption algorithm "%s". Please use one of the following one: %s', $keyEncryptionAlgorithm, \implode(', ', $jweBuilder->getKeyEncryptionAlgorithmManager()->list())));
249
        }
250
        if (!\in_array($contentEncryptionAlgorithm, $jweBuilder->getContentEncryptionAlgorithmManager()->list(), true)) {
251
            throw new \InvalidArgumentException(\Safe\sprintf('Unsupported content encryption algorithm "%s". Please use one of the following one: %s', $contentEncryptionAlgorithm, \implode(', ', $jweBuilder->getContentEncryptionAlgorithmManager()->list())));
252
        }
253
        $this->jweBuilder = $jweBuilder;
254
        $this->keyEncryptionAlgorithm = $keyEncryptionAlgorithm;
255
        $this->contentEncryptionAlgorithm = $contentEncryptionAlgorithm;
256
    }
257
258
    public function build(): string
259
    {
260
        if (null === $this->scope) {
261
            throw new \LogicException('It is mandatory to set the scope.');
262
        }
263
        $data = $this->userinfo->getUserinfo($this->client, $this->user, $this->userAccount, $this->redirectUri, $this->requestedClaims, $this->scope, $this->claimsLocales);
0 ignored issues
show
Documentation introduced by
$this->userAccount is of type object<OAuth2Framework\C...serAccount\UserAccount>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$this->redirectUri is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Unused Code introduced by
The call to UserInfo::getUserinfo() has too many arguments starting with $this->claimsLocales.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
264
        //$data = $this->updateClaimsWithAmrAndAcrInfo($data, $this->userAccount);
0 ignored issues
show
Unused Code Comprehensibility introduced by
63% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
265
        $data = $this->updateClaimsWithAuthenticationTime($data, $this->user, $this->requestedClaims);
0 ignored issues
show
Unused Code introduced by
The call to IdTokenBuilder::updateCl...ithAuthenticationTime() has too many arguments starting with $this->requestedClaims.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
266
        $data = $this->updateClaimsWithNonce($data);
267
        if (null !== $this->signatureAlgorithm) {
268
            $data = $this->updateClaimsWithJwtClaims($data);
269
            $data = $this->updateClaimsWithTokenHash($data);
270
            $data = $this->updateClaimsAudience($data);
271
            $result = $this->computeIdToken($data);
272
        } else {
273
            $result = \Safe\json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
274
        }
275
276
        if (null !== $this->keyEncryptionAlgorithm && null !== $this->contentEncryptionAlgorithm) {
277
            $result = $this->tryToEncrypt($this->client, $result);
278
        }
279
280
        return $result;
281
    }
282
283
    private function updateClaimsWithJwtClaims(array $claims): array
284
    {
285
        if (null === $this->expiresAt) {
286
            $this->expiresAt = (new \DateTimeImmutable())->setTimestamp(\time() + $this->lifetime);
287
        }
288
        $claims += [
289
            'iat' => \time(),
290
            'nbf' => \time(),
291
            'exp' => $this->expiresAt->getTimestamp(),
292
            'jti' => Base64Url::encode(\random_bytes(16)),
293
            'iss' => $this->issuer,
294
        ];
295
296
        return $claims;
297
    }
298
299
    private function updateClaimsWithAuthenticationTime(array $claims, array $requestedClaims): array
300
    {
301
        if ((true === $this->withAuthenticationTime || \array_key_exists('auth_time', $requestedClaims)) && null !== $this->userAccount->getLastLoginAt()) {
302
            $claims['auth_time'] = $this->userAccount->getLastLoginAt();
303
        }
304
305
        return $claims;
306
    }
307
308
    private function updateClaimsWithNonce(array $claims): array
309
    {
310
        if (null !== $this->nonce) {
311
            $claims['nonce'] = $this->nonce;
312
        }
313
314
        return $claims;
315
    }
316
317
    private function updateClaimsAudience(array $claims): array
318
    {
319
        $claims['aud'] = [
320
            $this->client->getPublicId()->getValue(),
321
            $this->issuer,
322
        ];
323
        $claims['azp'] = $this->client->getPublicId()->getValue();
324
325
        return $claims;
326
    }
327
328
    private function computeIdToken(array $claims): string
329
    {
330
        $signatureKey = $this->getSignatureKey($this->signatureAlgorithm);
331
        $header = $this->getHeaders($signatureKey, $this->signatureAlgorithm);
332
        $jsonConverter = new StandardConverter();
0 ignored issues
show
Deprecated Code introduced by
The class Jose\Component\Core\Converter\StandardConverter has been deprecated with message: This class is deprecated in v1.3 and will be removed in v2.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
333
        $claims = $jsonConverter->encode($claims);
334
        $jws = $this->jwsBuilder
335
            ->create()
336
            ->withPayload($claims)
337
            ->addSignature($signatureKey, $header)
338
            ->build();
339
        $serializer = new JwsCompactSerializer($jsonConverter);
340
341
        return $serializer->serialize($jws, 0);
342
    }
343
344
    private function tryToEncrypt(Client $client, string $jwt): string
345
    {
346
        $clientKeySet = $this->getClientKeySet($client);
347
        $keyEncryptionAlgorithm = $this->jweBuilder->getKeyEncryptionAlgorithmManager()->get($this->keyEncryptionAlgorithm);
348
        $encryptionKey = $clientKeySet->selectKey('enc', $keyEncryptionAlgorithm);
349
        if (null === $encryptionKey) {
350
            throw new \InvalidArgumentException('No encryption key available for the client.');
351
        }
352
        $header = [
353
            'typ' => 'JWT',
354
            'jti' => Base64Url::encode(\random_bytes(16)),
355
            'alg' => $this->keyEncryptionAlgorithm,
356
            'enc' => $this->contentEncryptionAlgorithm,
357
        ];
358
        $jwe = $this->jweBuilder
359
            ->create()
360
            ->withPayload($jwt)
361
            ->withSharedProtectedHeader($header)
362
            ->addRecipient($encryptionKey)
363
            ->build();
364
        $jsonConverter = new StandardConverter();
0 ignored issues
show
Deprecated Code introduced by
The class Jose\Component\Core\Converter\StandardConverter has been deprecated with message: This class is deprecated in v1.3 and will be removed in v2.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
365
        $serializer = new JweCompactSerializer($jsonConverter);
366
367
        return $serializer->serialize($jwe, 0);
368
    }
369
370
    private function getSignatureKey(string $signatureAlgorithm): JWK
371
    {
372
        $keys = $this->signatureKeys;
373
        if ($this->client->has('client_secret')) {
374
            $jwk = JWK::create([
375
                'kty' => 'oct',
376
                'use' => 'sig',
377
                'k' => Base64Url::encode($this->client->get('client_secret')),
378
            ]);
379
            $keys = $keys->with($jwk);
380
        }
381
        $signatureAlgorithm = $this->jwsBuilder->getSignatureAlgorithmManager()->get($signatureAlgorithm);
382
        if ('none' === $signatureAlgorithm->name()) {
383
            return JWK::create(['kty' => 'none', 'alg' => 'none', 'use' => 'sig']);
384
        }
385
        $signatureKey = $keys->selectKey('sig', $signatureAlgorithm);
386
        if (null === $signatureKey) {
387
            throw new \InvalidArgumentException('Unable to find a key to sign the ID Token. Please verify the selected key set contains suitable keys.');
388
        }
389
390
        return $signatureKey;
391
    }
392
393
    private function getHeaders(JWK $signatureKey, string $signatureAlgorithm): array
394
    {
395
        $header = [
396
            'typ' => 'JWT',
397
            'alg' => $signatureAlgorithm,
398
        ];
399
        if ($signatureKey->has('kid')) {
400
            $header['kid'] = $signatureKey->get('kid');
401
        }
402
403
        return $header;
404
    }
405
406
    private function updateClaimsWithTokenHash(array $claims): array
407
    {
408
        if ('none' === $this->signatureAlgorithm) {
409
            return $claims;
410
        }
411
        if (null !== $this->accessTokenId) {
412
            $claims['at_hash'] = $this->getHash($this->accessTokenId);
413
        }
414
        if (null !== $this->authorizationCodeId) {
415
            $claims['c_hash'] = $this->getHash($this->authorizationCodeId);
416
        }
417
418
        return $claims;
419
    }
420
421
    private function getHash(TokenId $tokenId): string
422
    {
423
        return Base64Url::encode(\mb_substr(\hash($this->getHashMethod(), $tokenId->getValue(), true), 0, $this->getHashSize(), '8bit'));
424
    }
425
426
    private function getHashMethod(): string
427
    {
428
        $map = [
429
            'HS256' => 'sha256',
430
            'ES256' => 'sha256',
431
            'RS256' => 'sha256',
432
            'PS256' => 'sha256',
433
            'HS384' => 'sha384',
434
            'ES384' => 'sha384',
435
            'RS384' => 'sha384',
436
            'PS384' => 'sha384',
437
            'HS512' => 'sha512',
438
            'ES512' => 'sha512',
439
            'RS512' => 'sha512',
440
            'PS512' => 'sha512',
441
        ];
442
443
        if (!\array_key_exists($this->signatureAlgorithm, $map)) {
444
            throw new \InvalidArgumentException(\Safe\sprintf('Algorithm "%s" is not supported', $this->signatureAlgorithm));
445
        }
446
447
        return $map[$this->signatureAlgorithm];
448
    }
449
450
    private function getHashSize(): int
451
    {
452
        $map = [
453
            'HS256' => 16,
454
            'ES256' => 16,
455
            'RS256' => 16,
456
            'PS256' => 16,
457
            'HS384' => 24,
458
            'ES384' => 24,
459
            'RS384' => 24,
460
            'PS384' => 24,
461
            'HS512' => 32,
462
            'ES512' => 32,
463
            'RS512' => 32,
464
            'PS512' => 32,
465
        ];
466
467
        if (!\array_key_exists($this->signatureAlgorithm, $map)) {
468
            throw new \InvalidArgumentException(\Safe\sprintf('Algorithm "%s" is not supported', $this->signatureAlgorithm));
469
        }
470
471
        return $map[$this->signatureAlgorithm];
472
    }
473
474
    private function getClientKeySet(Client $client): JWKSet
475
    {
476
        $keyset = JWKSet::createFromKeys([]);
477
        if ($client->has('jwks')) {
478
            $jwks = JWKSet::createFromJson($client->get('jwks'));
479
            foreach ($jwks as $jwk) {
480
                $keyset = $keyset->with($jwk);
481
            }
482
        }
483
        if ($client->has('client_secret')) {
484
            $jwk = JWK::create([
485
                'kty' => 'oct',
486
                'use' => 'enc',
487
                'k' => Base64Url::encode($client->get('client_secret')),
488
            ]);
489
            $keyset = $keyset->with($jwk);
490
        }
491
        if ($client->has('jwks_uri') && null !== $this->jkuFactory) {
492
            $jwksUri = $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
493
            foreach ($jwksUri as $jwk) {
494
                $keyset = $keyset->with($jwk);
495
            }
496
        }
497
498
        if (empty($keyset)) {
499
            throw new \InvalidArgumentException('The client has no key or key set.');
500
        }
501
502
        return $keyset;
503
    }
504
}
505