Failed Conditions
Push — ng ( a36945...8b0de7 )
by Florent
15:25
created

ClientAssertionJwt   F

Complexity

Total Complexity 53

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 22

Importance

Changes 0
Metric Value
wmc 53
lcom 1
cbo 22
dl 0
loc 344
rs 2.5397
c 0
b 0
f 0

16 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 40 7
B tryToDecryptClientAssertion() 0 21 5
A isClientAuthenticated() 0 15 3
A getSupportedMethods() 0 4 1
C checkClientConfiguration() 0 43 11
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
184
        try {
185
            if (!array_key_exists('client_assertion', $parameters)) {
186
                throw new \InvalidArgumentException('Parameter "client_assertion" is missing.');
187
            }
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
            if (1 !== $jws->countSignatures()) {
193
                throw new \InvalidArgumentException('The assertion must have only one signature.');
194
            }
195
            $this->headerCheckerManager->check($jws, 0);
196
            $claims = $this->jsonConverter->decode($jws->getPayload());
197
            $this->claimCheckerManager->check($claims);
198
199
            // FIXME: Other claims can be considered as mandatory
200
            $diff = array_diff(['iss', 'sub', 'aud', 'exp'], array_keys($claims));
201
            if (!empty($diff)) {
202
                throw new \InvalidArgumentException(sprintf('The following claim(s) is/are mandatory: "%s".', implode(', ', array_values($diff))));
203
            }
204
205
            $clientCredentials = $jws;
206
207
            return ClientId::create($claims['sub']);
208
        } catch (\Exception $e) {
209
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, $e->getMessage(), $e);
210
        }
211
    }
212
213
    /**
214
     * @param string $assertion
215
     *
216
     * @return string
217
     *
218
     * @throws OAuth2Exception
219
     */
220
    private function tryToDecryptClientAssertion(string $assertion): string
221
    {
222
        if (null === $this->jweLoader) {
223
            return $assertion;
224
        }
225
226
        try {
227
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($assertion, $this->keyEncryptionKeySet, $recipient);
0 ignored issues
show
Bug introduced by
It seems like $this->keyEncryptionKeySet can be null; however, loadAndDecryptWithKeySet() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
228
            if (1 !== $jwe->countRecipients()) {
229
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
230
            }
231
232
            return $jwe->getPayload();
233
        } catch (\Exception $e) {
234
            if (true === $this->encryptionRequired) {
235
                throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, $e->getMessage(), $e);
236
            }
237
238
            return $assertion;
239
        }
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
246
    {
247
        try {
248
            if (!$clientCredentials instanceof JWS) {
249
                return false;
250
            }
251
252
            $claims = $this->jsonConverter->decode($clientCredentials->getPayload());
253
            $jwkset = $this->retrieveIssuerKeySet($client, $clientCredentials, $claims);
254
255
            return $this->jwsVerifier->verifyWithKeySet($clientCredentials, $jwkset, 0);
256
        } catch (\Exception $e) {
257
            return false;
258
        }
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     */
264
    public function getSupportedMethods(): array
265
    {
266
        return ['client_secret_jwt', 'private_key_jwt'];
267
    }
268
269
    /**
270
     * {@inheritdoc}
271
     */
272
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
273
    {
274
        if ('client_secret_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
275
            $validatedParameters = $validatedParameters->with('client_secret', $this->createClientSecret());
276
            $validatedParameters = $validatedParameters->with('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : time() + $this->secretLifetime));
277
        } elseif ('private_key_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
278
            switch (true) {
279
                case !($commandParameters->has('jwks') xor $commandParameters->has('jwks_uri')):
280
                    throw new \InvalidArgumentException('The parameter "jwks" or "jwks_uri" must be set.');
281
                case $commandParameters->has('jwks'):
282
                    try {
283
                        JWKSet::createFromKeyData($commandParameters->get('jwks'));
284
                    } catch (\Exception $e) {
285
                        throw new \InvalidArgumentException('The parameter "jwks" must be a valid JWKSet object.', 0, $e);
286
                    }
287
                    $validatedParameters = $validatedParameters->with('jwks', $commandParameters->get('jwks'));
288
289
                    break;
290
                case $commandParameters->has('jwks_uri'):
291
                    if (null === $this->jkuFactory) {
292
                        throw new \InvalidArgumentException('Distant key sets cannot be used. Please use "jwks" instead of "jwks_uri".');
293
                    }
294
295
                    try {
296
                        $jwks = $this->jkuFactory->loadFromUrl($commandParameters->get('jwks_uri'));
297
                    } catch (\Exception $e) {
298
                        throw new \InvalidArgumentException('The parameter "jwks_uri" must be a valid uri to a JWK Set and at least one key.', 0, $e);
299
                    }
300
                    if (empty($jwks)) {
301
                        throw new \InvalidArgumentException('The distant key set is empty.');
302
                    }
303
                    $validatedParameters = $validatedParameters->with('jwks_uri', $commandParameters->get('jwks_uri'));
304
305
                    break;
306
                default:
307
                    throw new \InvalidArgumentException('Unsupported token endpoint authentication method.');
308
            }
309
        } else {
310
            throw new \InvalidArgumentException('Unsupported token endpoint authentication method.');
311
        }
312
313
        return $validatedParameters;
314
    }
315
316
    /**
317
     * @return string
318
     */
319
    private function createClientSecret(): string
320
    {
321
        return bin2hex(random_bytes(128));
322
    }
323
324
    /**
325
     * @param Client $client
326
     * @param JWS    $jws
327
     * @param array  $claims
328
     *
329
     * @return JWKSet
330
     */
331
    private function retrieveIssuerKeySet(Client $client, JWS $jws, array $claims): JWKSet
332
    {
333
        if ($claims['sub'] === $claims['iss']) { // The client is the issuer
334
            return $this->getClientKeySet($client);
335
        }
336
337
        if (null === $this->trustedIssuerRepository || null === $trustedIssuer = $this->trustedIssuerRepository->find($claims['iss'])) {
338
            throw new \InvalidArgumentException('Unable to retrieve the key set of the issuer.');
339
        }
340
341
        if (!in_array(self::CLIENT_ASSERTION_TYPE, $trustedIssuer->getAllowedAssertionTypes())) {
342
            throw new \InvalidArgumentException(sprintf('The assertion type "%s" is not allowed for that issuer.', self::CLIENT_ASSERTION_TYPE));
343
        }
344
345
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
346
        if (!in_array($signatureAlgorithm, $trustedIssuer->getAllowedSignatureAlgorithms())) {
347
            throw new \InvalidArgumentException(sprintf('The signature algorithm "%s" is not allowed for that issuer.', $signatureAlgorithm));
348
        }
349
350
        return $trustedIssuer->getJWKSet();
351
    }
352
353
    /**
354
     * @param Client $client
355
     *
356
     * @return JWKSet
357
     */
358
    private function getClientKeySet(Client $client): JWKSet
359
    {
360
        switch (true) {
361
            case $client->has('jwks') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod():
362
                return JWKSet::createFromJson($client->get('jwks'));
363
            case $client->has('client_secret') && 'client_secret_jwt' === $client->getTokenEndpointAuthenticationMethod():
364
                $jwk = JWK::create([
365
                    'kty' => 'oct',
366
                    'use' => 'sig',
367
                    'k' => Base64Url::encode($client->get('client_secret')),
368
                ]);
369
370
                return JWKSet::createFromKeys([$jwk]);
371
            case $client->has('jwks_uri') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod() && null !== $this->jkuFactory:
372
                return $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
373
            default:
374
                throw new \InvalidArgumentException('The client has no key or key set.');
375
        }
376
    }
377
}
378