Failed Conditions
Push — master ( c6baf0...a3629e )
by Florent
16:19
created

ClientAssertionJwt   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 287
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 22

Importance

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

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