Failed Conditions
Push — master ( 65a339...cf9b90 )
by Florent
03:13
created

ClientAssertionJwt   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 125
c 3
b 1
f 0
dl 0
loc 288
rs 5.04
wmc 57

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 7 1
B findClientIdAndCredentials() 0 40 7
A tryToDecryptClientAssertion() 0 22 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 14 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-2019 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 Assert\Assertion;
17
use Base64Url\Base64Url;
18
use Jose\Component\Checker\ClaimCheckerManager;
19
use Jose\Component\Checker\HeaderCheckerManager;
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
use function Safe\json_decode;
35
use function Safe\json_encode;
36
use function Safe\sprintf;
37
38
class ClientAssertionJwt implements AuthenticationMethod
39
{
40
    private const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
41
42
    /**
43
     * @var JWSVerifier
44
     */
45
    private $jwsVerifier;
46
47
    /**
48
     * @var null|TrustedIssuerRepository
49
     */
50
    private $trustedIssuerRepository;
51
52
    /**
53
     * @var null|JKUFactory
54
     */
55
    private $jkuFactory;
56
57
    /**
58
     * @var null|JWELoader
59
     */
60
    private $jweLoader;
61
62
    /**
63
     * @var null|JWKSet
64
     */
65
    private $keyEncryptionKeySet;
66
67
    /**
68
     * @var bool
69
     */
70
    private $encryptionRequired = false;
71
72
    /**
73
     * @var int
74
     */
75
    private $secretLifetime;
76
77
    /**
78
     * @var HeaderCheckerManager
79
     */
80
    private $headerCheckerManager;
81
82
    /**
83
     * @var ClaimCheckerManager
84
     */
85
    private $claimCheckerManager;
86
87
    public function __construct(JWSVerifier $jwsVerifier, HeaderCheckerManager $headerCheckerManager, ClaimCheckerManager $claimCheckerManager, int $secretLifetime = 0)
88
    {
89
        Assertion::greaterOrEqualThan($secretLifetime, 0, 'The secret lifetime must be at least 0 (= unlimited).');
90
        $this->jwsVerifier = $jwsVerifier;
91
        $this->headerCheckerManager = $headerCheckerManager;
92
        $this->claimCheckerManager = $claimCheckerManager;
93
        $this->secretLifetime = $secretLifetime;
94
    }
95
96
    public function enableTrustedIssuerSupport(TrustedIssuerRepository $trustedIssuerRepository): void
97
    {
98
        $this->trustedIssuerRepository = $trustedIssuerRepository;
99
    }
100
101
    public function enableJkuSupport(JKUFactory $jkuFactory): void
102
    {
103
        $this->jkuFactory = $jkuFactory;
104
    }
105
106
    public function enableEncryptedAssertions(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $encryptionRequired): void
107
    {
108
        $this->jweLoader = $jweLoader;
109
        $this->encryptionRequired = $encryptionRequired;
110
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
111
    }
112
113
    /**
114
     * @return string[]
115
     */
116
    public function getSupportedSignatureAlgorithms(): array
117
    {
118
        return $this->jwsVerifier->getSignatureAlgorithmManager()->list();
119
    }
120
121
    /**
122
     * @return string[]
123
     */
124
    public function getSupportedContentEncryptionAlgorithms(): array
125
    {
126
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
127
    }
128
129
    /**
130
     * @return string[]
131
     */
132
    public function getSupportedKeyEncryptionAlgorithms(): array
133
    {
134
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
135
    }
136
137
    public function getSchemesParameters(): array
138
    {
139
        return [];
140
    }
141
142
    /**
143
     * @param null|mixed $clientCredentials
144
     */
145
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ?ClientId
146
    {
147
        $parameters = RequestBodyParser::parseFormUrlEncoded($request);
148
        if (!\array_key_exists('client_assertion_type', $parameters)) {
149
            return null;
150
        }
151
        $clientAssertionType = $parameters['client_assertion_type'];
152
153
        if (self::CLIENT_ASSERTION_TYPE !== $clientAssertionType) {
154
            return null;
155
        }
156
        if (!\array_key_exists('client_assertion', $parameters)) {
157
            throw OAuth2Error::invalidRequest('Parameter "client_assertion" is missing.');
158
        }
159
160
        try {
161
            $client_assertion = $parameters['client_assertion'];
162
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
163
            $serializer = new CompactSerializer();
164
            $jws = $serializer->unserialize($client_assertion);
165
            $this->headerCheckerManager->check($jws, 0);
166
            $payload = $jws->getPayload();
167
            Assertion::string($payload, 'Unable to get the JWS payload');
168
            $claims = \Jose\Component\Core\Util\JsonConverter::decode($payload);
169
            $this->claimCheckerManager->check($claims);
170
        } catch (OAuth2Error $e) {
171
            throw $e;
172
        } catch (\Throwable $e) {
173
            throw OAuth2Error::invalidRequest('Unable to load, decrypt or verify the client assertion.', [], $e);
174
        }
175
176
        // FIXME: Other claims can be considered as mandatory by the server
177
        $diff = array_diff(['iss', 'sub', 'aud', 'exp'], array_keys($claims));
178
        if (0 !== \count($diff)) {
179
            throw OAuth2Error::invalidRequest(sprintf('The following claim(s) is/are mandatory: "%s".', implode(', ', array_values($diff))));
180
        }
181
182
        $clientCredentials = $jws;
183
184
        return new ClientId($claims['sub']);
185
    }
186
187
    /**
188
     * @param null|mixed $clientCredentials
189
     */
190
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
191
    {
192
        try {
193
            if (!$clientCredentials instanceof JWS) {
194
                return false;
195
            }
196
            $payload = $clientCredentials->getPayload();
197
            Assertion::string($payload, 'No payload available');
198
            $claims = \Jose\Component\Core\Util\JsonConverter::decode($payload);
199
            $jwkset = $this->retrieveIssuerKeySet($client, $clientCredentials, $claims);
200
201
            return $this->jwsVerifier->verifyWithKeySet($clientCredentials, $jwkset, 0);
202
        } catch (\Throwable $e) {
203
            return false;
204
        }
205
    }
206
207
    public function getSupportedMethods(): array
208
    {
209
        return ['client_secret_jwt', 'private_key_jwt'];
210
    }
211
212
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
213
    {
214
        switch ($commandParameters->get('token_endpoint_auth_method')) {
215
            case 'client_secret_jwt':
216
                return $this->checkClientSecretJwtConfiguration($commandParameters, $validatedParameters);
217
            case 'private_key_jwt':
218
                return $this->checkPrivateKeyJwtConfiguration($commandParameters, $validatedParameters);
219
            default:
220
                return $validatedParameters;
221
        }
222
    }
223
224
    private function tryToDecryptClientAssertion(string $assertion): string
225
    {
226
        if (null === $this->jweLoader) {
227
            return $assertion;
228
        }
229
230
        try {
231
            Assertion::notNull($this->keyEncryptionKeySet, 'The key encryption key set is not defined.');
232
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($assertion, $this->keyEncryptionKeySet, $recipient);
233
            if (1 !== $jwe->countRecipients()) {
234
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
235
            }
236
            $payload = $jwe->getPayload();
237
            Assertion::string($payload, 'Unable to get the JWE payload');
238
239
            return $payload;
240
        } catch (\Throwable $e) {
241
            if (true === $this->encryptionRequired) {
242
                throw OAuth2Error::invalidRequest('The encryption of the assertion is mandatory but the decryption of the assertion failed.', [], $e);
243
            }
244
245
            return $assertion;
246
        }
247
    }
248
249
    private function checkClientSecretJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
250
    {
251
        $validatedParameters->set('token_endpoint_auth_method', $commandParameters->get('token_endpoint_auth_method'));
252
        $validatedParameters->set('client_secret', $this->createClientSecret());
253
        $validatedParameters->set('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : time() + $this->secretLifetime));
254
255
        return $validatedParameters;
256
    }
257
258
    private function checkPrivateKeyJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
259
    {
260
        switch (true) {
261
            case $commandParameters->has('jwks') && $commandParameters->has('jwks_uri'):
262
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null === $this->trustedIssuerRepository:
263
                throw new \InvalidArgumentException('Either the parameter "jwks" or "jwks_uri" must be set.');
264
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null !== $this->trustedIssuerRepository: //Allowed when trusted issuer support is set
265
266
                break;
267
            case $commandParameters->has('jwks'):
268
                $validatedParameters->set('jwks', $commandParameters->get('jwks'));
269
270
                break;
271
            case $commandParameters->has('jwks_uri'):
272
                $validatedParameters->set('jwks_uri', $commandParameters->get('jwks_uri'));
273
274
                break;
275
        }
276
277
        return $validatedParameters;
278
    }
279
280
    private function createClientSecret(): string
281
    {
282
        return bin2hex(random_bytes(32));
283
    }
284
285
    private function retrieveIssuerKeySet(Client $client, JWS $jws, array $claims): JWKSet
286
    {
287
        if ($claims['sub'] === $claims['iss']) { // The client is the issuer
288
            return $this->getClientKeySet($client);
289
        }
290
291
        if (null === $this->trustedIssuerRepository || null === $trustedIssuer = $this->trustedIssuerRepository->find($claims['iss'])) {
292
            throw new \InvalidArgumentException('Unable to retrieve the key set of the issuer.');
293
        }
294
295
        if (!\in_array(self::CLIENT_ASSERTION_TYPE, $trustedIssuer->getAllowedAssertionTypes(), true)) {
296
            throw new \InvalidArgumentException(sprintf('The assertion type "%s" is not allowed for that issuer.', self::CLIENT_ASSERTION_TYPE));
297
        }
298
299
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
300
        if (!\in_array($signatureAlgorithm, $trustedIssuer->getAllowedSignatureAlgorithms(), true)) {
301
            throw new \InvalidArgumentException(sprintf('The signature algorithm "%s" is not allowed for that issuer.', $signatureAlgorithm));
302
        }
303
304
        return $trustedIssuer->getJWKSet();
305
    }
306
307
    private function getClientKeySet(Client $client): JWKSet
308
    {
309
        switch (true) {
310
            case $client->has('jwks') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod():
311
                $jwks = json_decode(json_encode($client->get('jwks'), JSON_FORCE_OBJECT), true);
312
313
                return JWKSet::createFromKeyData($jwks);
314
            case $client->has('client_secret') && 'client_secret_jwt' === $client->getTokenEndpointAuthenticationMethod():
315
                $jwk = new JWK([
316
                    'kty' => 'oct',
317
                    'use' => 'sig',
318
                    '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

318
                    'k' => Base64Url::encode(/** @scrutinizer ignore-type */ $client->get('client_secret')),
Loading history...
319
                ]);
320
321
                return new JWKSet([$jwk]);
322
            case $client->has('jwks_uri') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod() && null !== $this->jkuFactory:
323
                return $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
0 ignored issues
show
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

323
                return $this->jkuFactory->loadFromUrl(/** @scrutinizer ignore-type */ $client->get('jwks_uri'));
Loading history...
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

323
                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...
324
            default:
325
                throw new \InvalidArgumentException('The client has no key or key set.');
326
        }
327
    }
328
}
329