Failed Conditions
Push — master ( b8d841...bc596e )
by Florent
28:20
created

ClientAssertionJwt   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 272
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
wmc 58
lcom 1
cbo 21
dl 0
loc 272
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
    public function getSchemesParameters(): array
125
    {
126
        return [];
127
    }
128
129
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ?ClientId
130
    {
131
        $parameters = RequestBodyParser::parseFormUrlEncoded($request);
132
        if (!\array_key_exists('client_assertion_type', $parameters)) {
133
            return null;
134
        }
135
        $clientAssertionType = $parameters['client_assertion_type'];
136
137
        if (self::CLIENT_ASSERTION_TYPE !== $clientAssertionType) {
138
            return null;
139
        }
140
        if (!\array_key_exists('client_assertion', $parameters)) {
141
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, 'Parameter "client_assertion" is missing.');
142
        }
143
144
        try {
145
            $client_assertion = $parameters['client_assertion'];
146
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
147
            $serializer = new CompactSerializer($this->jsonConverter);
148
            $jws = $serializer->unserialize($client_assertion);
149
            $this->headerCheckerManager->check($jws, 0);
150
            $claims = $this->jsonConverter->decode($jws->getPayload());
151
            $this->claimCheckerManager->check($claims);
152
        } catch (OAuth2Message $e) {
153
            throw $e;
154
        } catch (\Exception $e) {
155
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, 'Unable to load, decrypt or verify the client assertion.', [], $e);
156
        }
157
158
        // FIXME: Other claims can be considered as mandatory by the server
159
        $diff = \array_diff(['iss', 'sub', 'aud', 'exp'], \array_keys($claims));
160
        if (!empty($diff)) {
161
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, \sprintf('The following claim(s) is/are mandatory: "%s".', \implode(', ', \array_values($diff))));
162
        }
163
164
        $clientCredentials = $jws;
165
166
        return new ClientId($claims['sub']);
167
    }
168
169
    /**
170
     * @throws OAuth2Message
171
     */
172
    private function tryToDecryptClientAssertion(string $assertion): string
173
    {
174
        if (null === $this->jweLoader) {
175
            return $assertion;
176
        }
177
178
        try {
179
            $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...
180
            if (1 !== $jwe->countRecipients()) {
181
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
182
            }
183
184
            return $jwe->getPayload();
185
        } catch (\Exception $e) {
186
            if (true === $this->encryptionRequired) {
187
                throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, 'The encryption of the assertion is mandatory but the decryption of the assertion failed.', [], $e);
188
            }
189
190
            return $assertion;
191
        }
192
    }
193
194
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
195
    {
196
        try {
197
            if (!$clientCredentials instanceof JWS) {
198
                return false;
199
            }
200
201
            $claims = $this->jsonConverter->decode($clientCredentials->getPayload());
202
            $jwkset = $this->retrieveIssuerKeySet($client, $clientCredentials, $claims);
203
204
            return $this->jwsVerifier->verifyWithKeySet($clientCredentials, $jwkset, 0);
205
        } catch (\Exception $e) {
206
            return false;
207
        }
208
    }
209
210
    public function getSupportedMethods(): array
211
    {
212
        return ['client_secret_jwt', 'private_key_jwt'];
213
    }
214
215
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
216
    {
217
        switch ($commandParameters->get('token_endpoint_auth_method')) {
218
            case 'client_secret_jwt':
219
                return $this->checkClientSecretJwtConfiguration($commandParameters, $validatedParameters);
220
            case 'private_key_jwt':
221
                return $this->checkPrivateKeyJwtConfiguration($commandParameters, $validatedParameters);
222
            default:
223
                return $validatedParameters;
224
        }
225
    }
226
227
    private function checkClientSecretJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
228
    {
229
        $validatedParameters->set('token_endpoint_auth_method', $commandParameters->get('token_endpoint_auth_method'));
230
        $validatedParameters->set('client_secret', $this->createClientSecret());
231
        $validatedParameters->set('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : \time() + $this->secretLifetime));
232
233
        return $validatedParameters;
234
    }
235
236
    private function checkPrivateKeyJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
237
    {
238
        switch (true) {
239
            case $commandParameters->has('jwks') && $commandParameters->has('jwks_uri'):
240
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null === $this->trustedIssuerRepository:
241
                throw new \InvalidArgumentException('Either the parameter "jwks" or "jwks_uri" must be set.');
242
            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...
243
244
                break;
245
            case $commandParameters->has('jwks'):
246
                $validatedParameters->set('jwks', $commandParameters->get('jwks'));
247
248
                break;
249
            case $commandParameters->has('jwks_uri'):
250
                $validatedParameters->set('jwks_uri', $commandParameters->get('jwks_uri'));
251
252
                break;
253
        }
254
255
        return $validatedParameters;
256
    }
257
258
    private function createClientSecret(): string
259
    {
260
        return \bin2hex(\random_bytes(32));
261
    }
262
263
    private function retrieveIssuerKeySet(Client $client, JWS $jws, array $claims): JWKSet
264
    {
265
        if ($claims['sub'] === $claims['iss']) { // The client is the issuer
266
            return $this->getClientKeySet($client);
267
        }
268
269
        if (null === $this->trustedIssuerRepository || null === $trustedIssuer = $this->trustedIssuerRepository->find($claims['iss'])) {
270
            throw new \InvalidArgumentException('Unable to retrieve the key set of the issuer.');
271
        }
272
273
        if (!\in_array(self::CLIENT_ASSERTION_TYPE, $trustedIssuer->getAllowedAssertionTypes(), true)) {
274
            throw new \InvalidArgumentException(\sprintf('The assertion type "%s" is not allowed for that issuer.', self::CLIENT_ASSERTION_TYPE));
275
        }
276
277
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
278
        if (!\in_array($signatureAlgorithm, $trustedIssuer->getAllowedSignatureAlgorithms(), true)) {
279
            throw new \InvalidArgumentException(\sprintf('The signature algorithm "%s" is not allowed for that issuer.', $signatureAlgorithm));
280
        }
281
282
        return $trustedIssuer->getJWKSet();
283
    }
284
285
    private function getClientKeySet(Client $client): JWKSet
286
    {
287
        switch (true) {
288
            case $client->has('jwks') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod():
289
                $jwks = \json_decode(\json_encode($client->get('jwks'), JSON_FORCE_OBJECT), true);
290
291
                return JWKSet::createFromKeyData($jwks);
292
            case $client->has('client_secret') && 'client_secret_jwt' === $client->getTokenEndpointAuthenticationMethod():
293
                $jwk = JWK::create([
294
                    'kty' => 'oct',
295
                    'use' => 'sig',
296
                    'k' => Base64Url::encode($client->get('client_secret')),
297
                ]);
298
299
                return JWKSet::createFromKeys([$jwk]);
300
            case $client->has('jwks_uri') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod() && null !== $this->jkuFactory:
301
                return $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
302
            default:
303
                throw new \InvalidArgumentException('The client has no key or key set.');
304
        }
305
    }
306
}
307