Failed Conditions
Push — master ( 819484...23fc45 )
by Florent
03:33
created

ClientAssertionJwt   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 285
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 121
dl 0
loc 285
rs 5.04
c 0
b 0
f 0
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 8 1
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 Assert\Assertion;
17
use Base64Url\Base64Url;
18
use Jose\Component\Checker\ClaimCheckerManager;
19
use Jose\Component\Checker\HeaderCheckerManager;
20
use Jose\Component\Core\Converter\JsonConverter;
21
use Jose\Component\Core\JWK;
22
use Jose\Component\Core\JWKSet;
23
use Jose\Component\Encryption\JWELoader;
24
use Jose\Component\KeyManagement\JKUFactory;
25
use Jose\Component\Signature\JWS;
26
use Jose\Component\Signature\JWSVerifier;
27
use Jose\Component\Signature\Serializer\CompactSerializer;
28
use OAuth2Framework\Component\Core\Client\Client;
29
use OAuth2Framework\Component\Core\Client\ClientId;
30
use OAuth2Framework\Component\Core\DataBag\DataBag;
31
use OAuth2Framework\Component\Core\Message\OAuth2Error;
32
use OAuth2Framework\Component\Core\TrustedIssuer\TrustedIssuerRepository;
33
use OAuth2Framework\Component\Core\Util\RequestBodyParser;
34
use Psr\Http\Message\ServerRequestInterface;
35
36
class ClientAssertionJwt implements AuthenticationMethod
37
{
38
    private const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
39
40
    private $jwsVerifier;
41
42
    /**
43
     * @var TrustedIssuerRepository|null
44
     */
45
    private $trustedIssuerRepository;
46
47
    /**
48
     * @var JKUFactory|null
49
     */
50
    private $jkuFactory;
51
52
    /**
53
     * @var JWELoader|null
54
     */
55
    private $jweLoader;
56
57
    /**
58
     * @var JWKSet|null
59
     */
60
    private $keyEncryptionKeySet;
61
62
    /**
63
     * @var bool
64
     */
65
    private $encryptionRequired = false;
66
67
    /**
68
     * @var int
69
     */
70
    private $secretLifetime;
71
72
    /**
73
     * @var HeaderCheckerManager
74
     */
75
    private $headerCheckerManager;
76
77
    /**
78
     * @var ClaimCheckerManager
79
     */
80
    private $claimCheckerManager;
81
82
    /**
83
     * @var JsonConverter
84
     */
85
    private $jsonConverter;
86
87
    public function __construct(JsonConverter $jsonConverter, 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->jsonConverter = $jsonConverter;
91
        $this->jwsVerifier = $jwsVerifier;
92
        $this->headerCheckerManager = $headerCheckerManager;
93
        $this->claimCheckerManager = $claimCheckerManager;
94
        $this->secretLifetime = $secretLifetime;
95
    }
96
97
    public function enableTrustedIssuerSupport(TrustedIssuerRepository $trustedIssuerRepository): void
98
    {
99
        $this->trustedIssuerRepository = $trustedIssuerRepository;
100
    }
101
102
    public function enableJkuSupport(JKUFactory $jkuFactory): void
103
    {
104
        $this->jkuFactory = $jkuFactory;
105
    }
106
107
    public function enableEncryptedAssertions(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $encryptionRequired): void
108
    {
109
        $this->jweLoader = $jweLoader;
110
        $this->encryptionRequired = $encryptionRequired;
111
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
112
    }
113
114
    /**
115
     * @return string[]
116
     */
117
    public function getSupportedSignatureAlgorithms(): array
118
    {
119
        return $this->jwsVerifier->getSignatureAlgorithmManager()->list();
120
    }
121
122
    /**
123
     * @return string[]
124
     */
125
    public function getSupportedContentEncryptionAlgorithms(): array
126
    {
127
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
128
    }
129
130
    /**
131
     * @return string[]
132
     */
133
    public function getSupportedKeyEncryptionAlgorithms(): array
134
    {
135
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
136
    }
137
138
    public function getSchemesParameters(): array
139
    {
140
        return [];
141
    }
142
143
    /**
144
     * @param mixed|null $clientCredentials
145
     */
146
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ?ClientId
147
    {
148
        $parameters = RequestBodyParser::parseFormUrlEncoded($request);
149
        if (!\array_key_exists('client_assertion_type', $parameters)) {
150
            return null;
151
        }
152
        $clientAssertionType = $parameters['client_assertion_type'];
153
154
        if (self::CLIENT_ASSERTION_TYPE !== $clientAssertionType) {
155
            return null;
156
        }
157
        if (!\array_key_exists('client_assertion', $parameters)) {
158
            throw OAuth2Error::invalidRequest('Parameter "client_assertion" is missing.');
159
        }
160
161
        try {
162
            $client_assertion = $parameters['client_assertion'];
163
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
164
            $serializer = new CompactSerializer($this->jsonConverter);
165
            $jws = $serializer->unserialize($client_assertion);
166
            $this->headerCheckerManager->check($jws, 0);
167
            $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

167
            $claims = $this->jsonConverter->decode(/** @scrutinizer ignore-type */ $jws->getPayload());
Loading history...
168
            $this->claimCheckerManager->check($claims);
169
        } catch (OAuth2Error $e) {
170
            throw $e;
171
        } catch (\Throwable $e) {
172
            throw OAuth2Error::invalidRequest('Unable to load, decrypt or verify the client assertion.', [], $e);
173
        }
174
175
        // FIXME: Other claims can be considered as mandatory by the server
176
        $diff = \array_diff(['iss', 'sub', 'aud', 'exp'], \array_keys($claims));
177
        if (0 !== \count($diff)) {
178
            throw OAuth2Error::invalidRequest(\Safe\sprintf('The following claim(s) is/are mandatory: "%s".', \implode(', ', \array_values($diff))));
179
        }
180
181
        $clientCredentials = $jws;
182
183
        return new ClientId($claims['sub']);
184
    }
185
186
    private function tryToDecryptClientAssertion(string $assertion): string
187
    {
188
        if (null === $this->jweLoader) {
189
            return $assertion;
190
        }
191
192
        try {
193
            $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

193
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($assertion, /** @scrutinizer ignore-type */ $this->keyEncryptionKeySet, $recipient);
Loading history...
194
            if (1 !== $jwe->countRecipients()) {
195
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
196
            }
197
198
            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...
199
        } catch (\Throwable $e) {
200
            if (true === $this->encryptionRequired) {
201
                throw OAuth2Error::invalidRequest('The encryption of the assertion is mandatory but the decryption of the assertion failed.', [], $e);
202
            }
203
204
            return $assertion;
205
        }
206
    }
207
208
    /**
209
     * @param mixed|null $clientCredentials
210
     */
211
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
212
    {
213
        try {
214
            if (!$clientCredentials instanceof JWS) {
215
                return false;
216
            }
217
218
            $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

218
            $claims = $this->jsonConverter->decode(/** @scrutinizer ignore-type */ $clientCredentials->getPayload());
Loading history...
219
            $jwkset = $this->retrieveIssuerKeySet($client, $clientCredentials, $claims);
220
221
            return $this->jwsVerifier->verifyWithKeySet($clientCredentials, $jwkset, 0);
222
        } catch (\Throwable $e) {
223
            return false;
224
        }
225
    }
226
227
    public function getSupportedMethods(): array
228
    {
229
        return ['client_secret_jwt', 'private_key_jwt'];
230
    }
231
232
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
233
    {
234
        switch ($commandParameters->get('token_endpoint_auth_method')) {
235
            case 'client_secret_jwt':
236
                return $this->checkClientSecretJwtConfiguration($commandParameters, $validatedParameters);
237
            case 'private_key_jwt':
238
                return $this->checkPrivateKeyJwtConfiguration($commandParameters, $validatedParameters);
239
            default:
240
                return $validatedParameters;
241
        }
242
    }
243
244
    private function checkClientSecretJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
245
    {
246
        $validatedParameters->set('token_endpoint_auth_method', $commandParameters->get('token_endpoint_auth_method'));
247
        $validatedParameters->set('client_secret', $this->createClientSecret());
248
        $validatedParameters->set('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : \time() + $this->secretLifetime));
249
250
        return $validatedParameters;
251
    }
252
253
    private function checkPrivateKeyJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
254
    {
255
        switch (true) {
256
            case $commandParameters->has('jwks') && $commandParameters->has('jwks_uri'):
257
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null === $this->trustedIssuerRepository:
258
                throw new \InvalidArgumentException('Either the parameter "jwks" or "jwks_uri" must be set.');
259
            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...
260
261
                break;
262
            case $commandParameters->has('jwks'):
263
                $validatedParameters->set('jwks', $commandParameters->get('jwks'));
264
265
                break;
266
            case $commandParameters->has('jwks_uri'):
267
                $validatedParameters->set('jwks_uri', $commandParameters->get('jwks_uri'));
268
269
                break;
270
        }
271
272
        return $validatedParameters;
273
    }
274
275
    private function createClientSecret(): string
276
    {
277
        return \bin2hex(\random_bytes(32));
278
    }
279
280
    private function retrieveIssuerKeySet(Client $client, JWS $jws, array $claims): JWKSet
281
    {
282
        if ($claims['sub'] === $claims['iss']) { // The client is the issuer
283
            return $this->getClientKeySet($client);
284
        }
285
286
        if (null === $this->trustedIssuerRepository || null === $trustedIssuer = $this->trustedIssuerRepository->find($claims['iss'])) {
287
            throw new \InvalidArgumentException('Unable to retrieve the key set of the issuer.');
288
        }
289
290
        if (!\in_array(self::CLIENT_ASSERTION_TYPE, $trustedIssuer->getAllowedAssertionTypes(), true)) {
291
            throw new \InvalidArgumentException(\Safe\sprintf('The assertion type "%s" is not allowed for that issuer.', self::CLIENT_ASSERTION_TYPE));
292
        }
293
294
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
295
        if (!\in_array($signatureAlgorithm, $trustedIssuer->getAllowedSignatureAlgorithms(), true)) {
296
            throw new \InvalidArgumentException(\Safe\sprintf('The signature algorithm "%s" is not allowed for that issuer.', $signatureAlgorithm));
297
        }
298
299
        return $trustedIssuer->getJWKSet();
300
    }
301
302
    private function getClientKeySet(Client $client): JWKSet
303
    {
304
        switch (true) {
305
            case $client->has('jwks') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod():
306
                $jwks = \Safe\json_decode(\Safe\json_encode($client->get('jwks'), JSON_FORCE_OBJECT), true);
307
308
                return JWKSet::createFromKeyData($jwks);
309
            case $client->has('client_secret') && 'client_secret_jwt' === $client->getTokenEndpointAuthenticationMethod():
310
                $jwk = JWK::create([
311
                    'kty' => 'oct',
312
                    'use' => 'sig',
313
                    '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

313
                    'k' => Base64Url::encode(/** @scrutinizer ignore-type */ $client->get('client_secret')),
Loading history...
314
                ]);
315
316
                return JWKSet::createFromKeys([$jwk]);
317
            case $client->has('jwks_uri') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod() && null !== $this->jkuFactory:
318
                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

318
                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

318
                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...
319
            default:
320
                throw new \InvalidArgumentException('The client has no key or key set.');
321
        }
322
    }
323
}
324