Failed Conditions
Push — ng ( eeabaf...fe50aa )
by Florent
05:34
created

ClientAssertionJwt   C

Complexity

Total Complexity 62

Size/Duplication

Total Lines 374
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 62
lcom 1
cbo 6
dl 0
loc 374
rs 5.9493
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 2
A enableTrustedIssuerSupport() 0 4 1
A enableJkuSupport() 0 4 1
A enableEncryptedAssertions() 0 6 1
A getSupportedSignatureAlgorithms() 0 4 1
A getSupportedContentEncryptionAlgorithms() 0 4 2
A getSupportedKeyEncryptionAlgorithms() 0 4 2
A getSchemesParameters() 0 4 1
C findClientIdAndCredentials() 0 39 7
B tryToDecryptClientAssertion() 0 21 5
A isClientAuthenticated() 0 15 3
A getSupportedMethods() 0 4 1
A checkClientConfiguration() 0 11 3
A processClientSecretJwtConfiguration() 0 8 2
C processPrivateKeyJwtConfiguration() 0 41 15
A createClientSecret() 0 4 1
B retrieveIssuerKeySet() 0 21 6
B getClientKeySet() 0 19 8

How to fix   Complexity   

Complex Class

Complex classes like ClientAssertionJwt 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 ClientAssertionJwt, 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\ClientAuthentication;
15
16
use Base64Url\Base64Url;
17
use Jose\Component\Checker\ClaimCheckerManager;
18
use Jose\Component\Checker\HeaderCheckerManager;
19
use Jose\Component\Core\Converter\JsonConverter;
20
use Jose\Component\Core\JWK;
21
use Jose\Component\Core\JWKSet;
22
use Jose\Component\Encryption\JWELoader;
23
use Jose\Component\KeyManagement\JKUFactory;
24
use Jose\Component\Signature\JWS;
25
use Jose\Component\Signature\JWSVerifier;
26
use Jose\Component\Signature\Serializer\CompactSerializer;
27
use OAuth2Framework\Component\Core\Client\Client;
28
use OAuth2Framework\Component\Core\Client\ClientId;
29
use OAuth2Framework\Component\Core\DataBag\DataBag;
30
use OAuth2Framework\Component\Core\Exception\OAuth2Exception;
31
use OAuth2Framework\Component\TrustedIssuer\TrustedIssuerRepository;
32
use Psr\Http\Message\ServerRequestInterface;
33
34
class ClientAssertionJwt implements AuthenticationMethod
35
{
36
    private const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
37
38
    /**
39
     * @var JWSVerifier
40
     */
41
    private $jwsVerifier;
42
43
    /**
44
     * @var null|TrustedIssuerRepository
45
     */
46
    private $trustedIssuerRepository = null;
47
48
    /**
49
     * @var null|JKUFactory
50
     */
51
    private $jkuFactory = null;
52
53
    /**
54
     * @var null|JWELoader
55
     */
56
    private $jweLoader = null;
57
58
    /**
59
     * @var null|JWKSet
60
     */
61
    private $keyEncryptionKeySet = null;
62
63
    /**
64
     * @var bool
65
     */
66
    private $encryptionRequired = false;
67
68
    /**
69
     * @var int
70
     */
71
    private $secretLifetime;
72
73
    /**
74
     * @var HeaderCheckerManager
75
     */
76
    private $headerCheckerManager;
77
78
    /**
79
     * @var ClaimCheckerManager
80
     */
81
    private $claimCheckerManager;
82
83
    /**
84
     * @var JsonConverter
85
     */
86
    private $jsonConverter;
87
88
    /**
89
     * ClientAssertionJwt constructor.
90
     *
91
     * @param JsonConverter        $jsonConverter
92
     * @param JWSVerifier          $jwsVerifier
93
     * @param HeaderCheckerManager $headerCheckerManager
94
     * @param ClaimCheckerManager  $claimCheckerManager
95
     * @param int                  $secretLifetime
96
     */
97
    public function __construct(JsonConverter $jsonConverter, JWSVerifier $jwsVerifier, HeaderCheckerManager $headerCheckerManager, ClaimCheckerManager $claimCheckerManager, int $secretLifetime = 0)
98
    {
99
        if ($secretLifetime < 0) {
100
            throw new \InvalidArgumentException('The secret lifetime must be at least 0 (= unlimited).');
101
        }
102
        $this->jsonConverter = $jsonConverter;
103
        $this->jwsVerifier = $jwsVerifier;
104
        $this->headerCheckerManager = $headerCheckerManager;
105
        $this->claimCheckerManager = $claimCheckerManager;
106
        $this->secretLifetime = $secretLifetime;
107
    }
108
109
    /**
110
     * @param TrustedIssuerRepository $trustedIssuerRepository
111
     */
112
    public function enableTrustedIssuerSupport(TrustedIssuerRepository $trustedIssuerRepository)
113
    {
114
        $this->trustedIssuerRepository = $trustedIssuerRepository;
115
    }
116
117
    /**
118
     * @param JKUFactory $jkuFactory
119
     */
120
    public function enableJkuSupport(JKUFactory $jkuFactory)
121
    {
122
        $this->jkuFactory = $jkuFactory;
123
    }
124
125
    /**
126
     * @param JWELoader $jweLoader
127
     * @param JWKSet    $keyEncryptionKeySet
128
     * @param bool      $encryptionRequired
129
     */
130
    public function enableEncryptedAssertions(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $encryptionRequired)
131
    {
132
        $this->jweLoader = $jweLoader;
133
        $this->encryptionRequired = $encryptionRequired;
134
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
135
    }
136
137
    /**
138
     * @return string[]
139
     */
140
    public function getSupportedSignatureAlgorithms(): array
141
    {
142
        return $this->jwsVerifier->getSignatureAlgorithmManager()->list();
143
    }
144
145
    /**
146
     * @return string[]
147
     */
148
    public function getSupportedContentEncryptionAlgorithms(): array
149
    {
150
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
151
    }
152
153
    /**
154
     * @return string[]
155
     */
156
    public function getSupportedKeyEncryptionAlgorithms(): array
157
    {
158
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function getSchemesParameters(): array
165
    {
166
        return [];
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ? ClientId
173
    {
174
        $parameters = $request->getParsedBody() ?? [];
175
        if (!array_key_exists('client_assertion_type', $parameters)) {
176
            return null;
177
        }
178
        $clientAssertionType = $parameters['client_assertion_type'];
179
180
        if (self::CLIENT_ASSERTION_TYPE !== $clientAssertionType) {
181
            return null;
182
        }
183
        if (!array_key_exists('client_assertion', $parameters)) {
184
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, 'Parameter "client_assertion" is missing.');
185
        }
186
187
        try {
188
            $client_assertion = $parameters['client_assertion'];
189
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
190
            $serializer = new CompactSerializer($this->jsonConverter);
191
            $jws = $serializer->unserialize($client_assertion);
192
            $this->headerCheckerManager->check($jws, 0);
193
            $claims = $this->jsonConverter->decode($jws->getPayload());
194
            $this->claimCheckerManager->check($claims);
195
        } catch (OAuth2Exception $e) {
196
            throw $e;
197
        } catch (\Exception $e) {
198
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, 'Unable to load, decrypt or verify the client assertion.', $e);
199
        }
200
201
        // FIXME: Other claims can be considered as mandatory by the server
202
        $diff = array_diff(['iss', 'sub', 'aud', 'exp'], array_keys($claims));
203
        if (!empty($diff)) {
204
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, sprintf('The following claim(s) is/are mandatory: "%s".', implode(', ', array_values($diff))));
205
        }
206
207
        $clientCredentials = $jws;
208
209
        return ClientId::create($claims['sub']);
210
    }
211
212
    /**
213
     * @param string $assertion
214
     *
215
     * @return string
216
     *
217
     * @throws OAuth2Exception
218
     */
219
    private function tryToDecryptClientAssertion(string $assertion): string
220
    {
221
        if (null === $this->jweLoader) {
222
            return $assertion;
223
        }
224
225
        try {
226
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($assertion, $this->keyEncryptionKeySet, $recipient);
0 ignored issues
show
Bug introduced by
The variable $recipient 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...
227
            if (1 !== $jwe->countRecipients()) {
228
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
229
            }
230
231
            return $jwe->getPayload();
232
        } catch (\Exception $e) {
233
            if (true === $this->encryptionRequired) {
234
                throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, 'The encryption of the assertion is mandatory but the decryption of the assertion failed.', $e);
235
            }
236
237
            return $assertion;
238
        }
239
    }
240
241
    /**
242
     * {@inheritdoc}
243
     */
244
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
245
    {
246
        try {
247
            if (!$clientCredentials instanceof JWS) {
0 ignored issues
show
Bug introduced by
The class Jose\Component\Signature\JWS does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
248
                return false;
249
            }
250
251
            $claims = $this->jsonConverter->decode($clientCredentials->getPayload());
252
            $jwkset = $this->retrieveIssuerKeySet($client, $clientCredentials, $claims);
253
254
            return $this->jwsVerifier->verifyWithKeySet($clientCredentials, $jwkset, 0);
255
        } catch (\Exception $e) {
256
            return false;
257
        }
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263
    public function getSupportedMethods(): array
264
    {
265
        return ['client_secret_jwt', 'private_key_jwt'];
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
272
    {
273
        switch ($commandParameters->get('token_endpoint_auth_method')) {
274
            case 'client_secret_jwt':
275
                return $this->processClientSecretJwtConfiguration($commandParameters, $validatedParameters);
276
            case 'private_key_jwt':
277
                return $this->processPrivateKeyJwtConfiguration($commandParameters, $validatedParameters);
278
            default:
279
                return $validatedParameters;
280
        }
281
    }
282
283
    /**
284
     * @param DataBag $commandParameters
285
     * @param DataBag $validatedParameters
286
     *
287
     * @return DataBag
288
     */
289
    private function processClientSecretJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
290
    {
291
        $validatedParameters = $validatedParameters->with('token_endpoint_auth_method', $commandParameters->get('token_endpoint_auth_method'));
292
        $validatedParameters = $validatedParameters->with('client_secret', $this->createClientSecret());
293
        $validatedParameters = $validatedParameters->with('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : time() + $this->secretLifetime));
294
295
        return $validatedParameters;
296
    }
297
298
    /**
299
     * @param DataBag $commandParameters
300
     * @param DataBag $validatedParameters
301
     *
302
     * @return DataBag
303
     */
304
    private function processPrivateKeyJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
305
    {
306
        switch (true) {
307
            case $commandParameters->has('jwks') && $commandParameters->has('jwks_uri'):
308
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null === $this->trustedIssuerRepository:
309
                throw new \InvalidArgumentException('Either the parameter "jwks" or "jwks_uri" must be set.');
310
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null !== $this->trustedIssuerRepository: //Allowed when trusted issuer support is set
311
                $validatedParameters = $validatedParameters->with('token_endpoint_auth_method', $commandParameters->get('token_endpoint_auth_method'));
312
313
                break;
314
            case $commandParameters->has('jwks'):
315
                try {
316
                    JWKSet::createFromJson($commandParameters->get('jwks'));
317
                } catch (\Throwable $e) {
318
                    throw new \InvalidArgumentException('The parameter "jwks" must be a valid JWKSet object.', 0, $e);
319
                }
320
                $validatedParameters = $validatedParameters->with('token_endpoint_auth_method', $commandParameters->get('token_endpoint_auth_method'));
321
                $validatedParameters = $validatedParameters->with('jwks', $commandParameters->get('jwks'));
322
323
                break;
324
            case $commandParameters->has('jwks_uri'):
325
                if (null === $this->jkuFactory) {
326
                    throw new \InvalidArgumentException('Distant key sets cannot be used. Please use "jwks" instead of "jwks_uri".');
327
                }
328
329
                try {
330
                    $jwks = $this->jkuFactory->loadFromUrl($commandParameters->get('jwks_uri'));
331
                } catch (\Exception $e) {
332
                    throw new \InvalidArgumentException('The parameter "jwks_uri" must be a valid uri to a JWKSet.', 0, $e);
333
                }
334
                if (0 === $jwks->count()) {
335
                    throw new \InvalidArgumentException('The distant key set is empty.');
336
                }
337
                $validatedParameters = $validatedParameters->with('token_endpoint_auth_method', $commandParameters->get('token_endpoint_auth_method'));
338
                $validatedParameters = $validatedParameters->with('jwks_uri', $commandParameters->get('jwks_uri'));
339
340
                break;
341
        }
342
343
        return $validatedParameters;
344
    }
345
346
    /**
347
     * @return string
348
     */
349
    private function createClientSecret(): string
350
    {
351
        return bin2hex(random_bytes(128));
352
    }
353
354
    /**
355
     * @param Client $client
356
     * @param JWS    $jws
357
     * @param array  $claims
358
     *
359
     * @return JWKSet
360
     */
361
    private function retrieveIssuerKeySet(Client $client, JWS $jws, array $claims): JWKSet
362
    {
363
        if ($claims['sub'] === $claims['iss']) { // The client is the issuer
364
            return $this->getClientKeySet($client);
365
        }
366
367
        if (null === $this->trustedIssuerRepository || null === $trustedIssuer = $this->trustedIssuerRepository->find($claims['iss'])) {
368
            throw new \InvalidArgumentException('Unable to retrieve the key set of the issuer.');
369
        }
370
371
        if (!in_array(self::CLIENT_ASSERTION_TYPE, $trustedIssuer->getAllowedAssertionTypes())) {
372
            throw new \InvalidArgumentException(sprintf('The assertion type "%s" is not allowed for that issuer.', self::CLIENT_ASSERTION_TYPE));
373
        }
374
375
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
376
        if (!in_array($signatureAlgorithm, $trustedIssuer->getAllowedSignatureAlgorithms())) {
377
            throw new \InvalidArgumentException(sprintf('The signature algorithm "%s" is not allowed for that issuer.', $signatureAlgorithm));
378
        }
379
380
        return $trustedIssuer->getJWKSet();
381
    }
382
383
    /**
384
     * @param Client $client
385
     *
386
     * @return JWKSet
387
     */
388
    private function getClientKeySet(Client $client): JWKSet
389
    {
390
        switch (true) {
391
            case $client->has('jwks') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod():
392
                return JWKSet::createFromJson($client->get('jwks'));
393
            case $client->has('client_secret') && 'client_secret_jwt' === $client->getTokenEndpointAuthenticationMethod():
394
                $jwk = JWK::create([
395
                    'kty' => 'oct',
396
                    'use' => 'sig',
397
                    'k' => Base64Url::encode($client->get('client_secret')),
398
                ]);
399
400
                return JWKSet::createFromKeys([$jwk]);
401
            case $client->has('jwks_uri') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod() && null !== $this->jkuFactory:
402
                return $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
403
            default:
404
                throw new \InvalidArgumentException('The client has no key or key set.');
405
        }
406
    }
407
}
408