Failed Conditions
Push — master ( aacec5...b5a0b4 )
by Florent
04:50
created

ClientAssertionJwt   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 284
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
wmc 58
lcom 1
cbo 21
dl 0
loc 284
rs 4.5599
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getSupportedMethods() 0 4 1
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
B findClientIdAndCredentials() 0 39 7
A tryToDecryptClientAssertion() 0 21 5
A isClientAuthenticated() 0 15 3
A checkClientConfiguration() 0 11 3
A checkClientSecretJwtConfiguration() 0 8 2
B checkPrivateKeyJwtConfiguration() 0 21 11
A createClientSecret() 0 4 1
B retrieveIssuerKeySet() 0 21 6
B getClientKeySet() 0 21 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\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());
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 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...
192
            if (1 !== $jwe->countRecipients()) {
193
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
194
            }
195
196
            return $jwe->getPayload();
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());
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')),
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'));
314
            default:
315
                throw new \InvalidArgumentException('The client has no key or key set.');
316
        }
317
    }
318
}
319