Failed Conditions
Push — master ( b5a0b4...819484 )
by Florent
08:00
created

ClientAssertionJwt   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 281
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 122
dl 0
loc 281
rs 4.5599
c 0
b 0
f 0
wmc 58

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getSupportedMethods() 0 3 1
A checkClientConfiguration() 0 9 3
A enableTrustedIssuerSupport() 0 3 1
A createClientSecret() 0 3 1
A __construct() 0 10 2
B findClientIdAndCredentials() 0 38 7
A tryToDecryptClientAssertion() 0 19 5
A enableEncryptedAssertions() 0 5 1
A getSupportedContentEncryptionAlgorithms() 0 3 2
A getSchemesParameters() 0 3 1
B checkPrivateKeyJwtConfiguration() 0 20 11
A enableJkuSupport() 0 3 1
B getClientKeySet() 0 19 8
A isClientAuthenticated() 0 13 3
A getSupportedSignatureAlgorithms() 0 3 1
A checkClientSecretJwtConfiguration() 0 7 2
A getSupportedKeyEncryptionAlgorithms() 0 3 2
A retrieveIssuerKeySet() 0 20 6

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.

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\Message\OAuth2Error;
31
use OAuth2Framework\Component\Core\TrustedIssuer\TrustedIssuerRepository;
32
use OAuth2Framework\Component\Core\Util\RequestBodyParser;
33
use Psr\Http\Message\ServerRequestInterface;
34
35
class ClientAssertionJwt implements AuthenticationMethod
36
{
37
    private const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
38
39
    private $jwsVerifier;
40
41
    /**
42
     * @var TrustedIssuerRepository|null
43
     */
44
    private $trustedIssuerRepository = null;
45
46
    /**
47
     * @var JKUFactory|null
48
     */
49
    private $jkuFactory = null;
50
51
    /**
52
     * @var JWELoader|null
53
     */
54
    private $jweLoader = null;
55
56
    /**
57
     * @var JWKSet|null
58
     */
59
    private $keyEncryptionKeySet = null;
60
61
    /**
62
     * @var bool
63
     */
64
    private $encryptionRequired = false;
65
66
    /**
67
     * @var int
68
     */
69
    private $secretLifetime;
70
71
    /**
72
     * @var HeaderCheckerManager
73
     */
74
    private $headerCheckerManager;
75
76
    /**
77
     * @var ClaimCheckerManager
78
     */
79
    private $claimCheckerManager;
80
81
    /**
82
     * @var JsonConverter
83
     */
84
    private $jsonConverter;
85
86
    public function __construct(JsonConverter $jsonConverter, JWSVerifier $jwsVerifier, HeaderCheckerManager $headerCheckerManager, ClaimCheckerManager $claimCheckerManager, int $secretLifetime = 0)
87
    {
88
        if ($secretLifetime < 0) {
89
            throw new \InvalidArgumentException('The secret lifetime must be at least 0 (= unlimited).');
90
        }
91
        $this->jsonConverter = $jsonConverter;
92
        $this->jwsVerifier = $jwsVerifier;
93
        $this->headerCheckerManager = $headerCheckerManager;
94
        $this->claimCheckerManager = $claimCheckerManager;
95
        $this->secretLifetime = $secretLifetime;
96
    }
97
98
    public function enableTrustedIssuerSupport(TrustedIssuerRepository $trustedIssuerRepository): void
99
    {
100
        $this->trustedIssuerRepository = $trustedIssuerRepository;
101
    }
102
103
    public function enableJkuSupport(JKUFactory $jkuFactory): void
104
    {
105
        $this->jkuFactory = $jkuFactory;
106
    }
107
108
    public function enableEncryptedAssertions(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $encryptionRequired): void
109
    {
110
        $this->jweLoader = $jweLoader;
111
        $this->encryptionRequired = $encryptionRequired;
112
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
113
    }
114
115
    /**
116
     * @return string[]
117
     */
118
    public function getSupportedSignatureAlgorithms(): array
119
    {
120
        return $this->jwsVerifier->getSignatureAlgorithmManager()->list();
121
    }
122
123
    /**
124
     * @return string[]
125
     */
126
    public function getSupportedContentEncryptionAlgorithms(): array
127
    {
128
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
129
    }
130
131
    /**
132
     * @return string[]
133
     */
134
    public function getSupportedKeyEncryptionAlgorithms(): array
135
    {
136
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
137
    }
138
139
    public function getSchemesParameters(): array
140
    {
141
        return [];
142
    }
143
144
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ?ClientId
145
    {
146
        $parameters = RequestBodyParser::parseFormUrlEncoded($request);
147
        if (!\array_key_exists('client_assertion_type', $parameters)) {
148
            return null;
149
        }
150
        $clientAssertionType = $parameters['client_assertion_type'];
151
152
        if (self::CLIENT_ASSERTION_TYPE !== $clientAssertionType) {
153
            return null;
154
        }
155
        if (!\array_key_exists('client_assertion', $parameters)) {
156
            throw OAuth2Error::invalidRequest('Parameter "client_assertion" is missing.');
157
        }
158
159
        try {
160
            $client_assertion = $parameters['client_assertion'];
161
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
162
            $serializer = new CompactSerializer($this->jsonConverter);
163
            $jws = $serializer->unserialize($client_assertion);
164
            $this->headerCheckerManager->check($jws, 0);
165
            $claims = $this->jsonConverter->decode($jws->getPayload());
0 ignored issues
show
Bug introduced by
It seems like $jws->getPayload() can also be of type null; however, parameter $payload of Jose\Component\Core\Conv...JsonConverter::decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

165
            $claims = $this->jsonConverter->decode(/** @scrutinizer ignore-type */ $jws->getPayload());
Loading history...
166
            $this->claimCheckerManager->check($claims);
167
        } catch (OAuth2Error $e) {
168
            throw $e;
169
        } catch (\Exception $e) {
170
            throw OAuth2Error::invalidRequest('Unable to load, decrypt or verify the client assertion.', [], $e);
171
        }
172
173
        // FIXME: Other claims can be considered as mandatory by the server
174
        $diff = \array_diff(['iss', 'sub', 'aud', 'exp'], \array_keys($claims));
175
        if (!empty($diff)) {
176
            throw OAuth2Error::invalidRequest(\Safe\sprintf('The following claim(s) is/are mandatory: "%s".', \implode(', ', \array_values($diff))));
177
        }
178
179
        $clientCredentials = $jws;
180
181
        return new ClientId($claims['sub']);
182
    }
183
184
    private function tryToDecryptClientAssertion(string $assertion): string
185
    {
186
        if (null === $this->jweLoader) {
187
            return $assertion;
188
        }
189
190
        try {
191
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($assertion, $this->keyEncryptionKeySet, $recipient);
0 ignored issues
show
Bug introduced by
It seems like $this->keyEncryptionKeySet can also be of type null; however, parameter $keyset of Jose\Component\Encryptio...dAndDecryptWithKeySet() does only seem to accept Jose\Component\Core\JWKSet, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

191
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($assertion, /** @scrutinizer ignore-type */ $this->keyEncryptionKeySet, $recipient);
Loading history...
192
            if (1 !== $jwe->countRecipients()) {
193
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
194
            }
195
196
            return $jwe->getPayload();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $jwe->getPayload() could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
197
        } catch (\Exception $e) {
198
            if (true === $this->encryptionRequired) {
199
                throw OAuth2Error::invalidRequest('The encryption of the assertion is mandatory but the decryption of the assertion failed.', [], $e);
200
            }
201
202
            return $assertion;
203
        }
204
    }
205
206
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
207
    {
208
        try {
209
            if (!$clientCredentials instanceof JWS) {
210
                return false;
211
            }
212
213
            $claims = $this->jsonConverter->decode($clientCredentials->getPayload());
0 ignored issues
show
Bug introduced by
It seems like $clientCredentials->getPayload() can also be of type null; however, parameter $payload of Jose\Component\Core\Conv...JsonConverter::decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

213
            $claims = $this->jsonConverter->decode(/** @scrutinizer ignore-type */ $clientCredentials->getPayload());
Loading history...
214
            $jwkset = $this->retrieveIssuerKeySet($client, $clientCredentials, $claims);
215
216
            return $this->jwsVerifier->verifyWithKeySet($clientCredentials, $jwkset, 0);
217
        } catch (\Exception $e) {
218
            return false;
219
        }
220
    }
221
222
    public function getSupportedMethods(): array
223
    {
224
        return ['client_secret_jwt', 'private_key_jwt'];
225
    }
226
227
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
228
    {
229
        switch ($commandParameters->get('token_endpoint_auth_method')) {
230
            case 'client_secret_jwt':
231
                return $this->checkClientSecretJwtConfiguration($commandParameters, $validatedParameters);
232
            case 'private_key_jwt':
233
                return $this->checkPrivateKeyJwtConfiguration($commandParameters, $validatedParameters);
234
            default:
235
                return $validatedParameters;
236
        }
237
    }
238
239
    private function checkClientSecretJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
240
    {
241
        $validatedParameters->set('token_endpoint_auth_method', $commandParameters->get('token_endpoint_auth_method'));
242
        $validatedParameters->set('client_secret', $this->createClientSecret());
243
        $validatedParameters->set('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : \time() + $this->secretLifetime));
244
245
        return $validatedParameters;
246
    }
247
248
    private function checkPrivateKeyJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
249
    {
250
        switch (true) {
251
            case $commandParameters->has('jwks') && $commandParameters->has('jwks_uri'):
252
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null === $this->trustedIssuerRepository:
253
                throw new \InvalidArgumentException('Either the parameter "jwks" or "jwks_uri" must be set.');
254
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null !== $this->trustedIssuerRepository: //Allowed when trusted issuer support is set
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
255
256
                break;
257
            case $commandParameters->has('jwks'):
258
                $validatedParameters->set('jwks', $commandParameters->get('jwks'));
259
260
                break;
261
            case $commandParameters->has('jwks_uri'):
262
                $validatedParameters->set('jwks_uri', $commandParameters->get('jwks_uri'));
263
264
                break;
265
        }
266
267
        return $validatedParameters;
268
    }
269
270
    private function createClientSecret(): string
271
    {
272
        return \bin2hex(\random_bytes(32));
273
    }
274
275
    private function retrieveIssuerKeySet(Client $client, JWS $jws, array $claims): JWKSet
276
    {
277
        if ($claims['sub'] === $claims['iss']) { // The client is the issuer
278
            return $this->getClientKeySet($client);
279
        }
280
281
        if (null === $this->trustedIssuerRepository || null === $trustedIssuer = $this->trustedIssuerRepository->find($claims['iss'])) {
282
            throw new \InvalidArgumentException('Unable to retrieve the key set of the issuer.');
283
        }
284
285
        if (!\in_array(self::CLIENT_ASSERTION_TYPE, $trustedIssuer->getAllowedAssertionTypes(), true)) {
286
            throw new \InvalidArgumentException(\Safe\sprintf('The assertion type "%s" is not allowed for that issuer.', self::CLIENT_ASSERTION_TYPE));
287
        }
288
289
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
290
        if (!\in_array($signatureAlgorithm, $trustedIssuer->getAllowedSignatureAlgorithms(), true)) {
291
            throw new \InvalidArgumentException(\Safe\sprintf('The signature algorithm "%s" is not allowed for that issuer.', $signatureAlgorithm));
292
        }
293
294
        return $trustedIssuer->getJWKSet();
295
    }
296
297
    private function getClientKeySet(Client $client): JWKSet
298
    {
299
        switch (true) {
300
            case $client->has('jwks') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod():
301
                $jwks = \Safe\json_decode(\Safe\json_encode($client->get('jwks'), JSON_FORCE_OBJECT), true);
302
303
                return JWKSet::createFromKeyData($jwks);
304
            case $client->has('client_secret') && 'client_secret_jwt' === $client->getTokenEndpointAuthenticationMethod():
305
                $jwk = JWK::create([
306
                    'kty' => 'oct',
307
                    'use' => 'sig',
308
                    'k' => Base64Url::encode($client->get('client_secret')),
0 ignored issues
show
Bug introduced by
It seems like $client->get('client_secret') can also be of type null; however, parameter $data of Base64Url\Base64Url::encode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

308
                    'k' => Base64Url::encode(/** @scrutinizer ignore-type */ $client->get('client_secret')),
Loading history...
309
                ]);
310
311
                return JWKSet::createFromKeys([$jwk]);
312
            case $client->has('jwks_uri') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod() && null !== $this->jkuFactory:
313
                return $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
0 ignored issues
show
Bug introduced by
The method loadFromUrl() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

313
                return $this->jkuFactory->/** @scrutinizer ignore-call */ loadFromUrl($client->get('jwks_uri'));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
It seems like $client->get('jwks_uri') can also be of type null; however, parameter $url of Jose\Component\KeyManage...UFactory::loadFromUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

313
                return $this->jkuFactory->loadFromUrl(/** @scrutinizer ignore-type */ $client->get('jwks_uri'));
Loading history...
314
            default:
315
                throw new \InvalidArgumentException('The client has no key or key set.');
316
        }
317
    }
318
}
319