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

AuthorizationRequestLoader   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 361
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
wmc 69
lcom 1
cbo 21
dl 0
loc 361
rs 2.88
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A isRequestUriRegistrationRequired() 0 4 1
A isRequestObjectSupportEnabled() 0 4 1
A isRequestObjectReferenceSupportEnabled() 0 4 1
A getSupportedSignatureAlgorithms() 0 4 2
A getSupportedKeyEncryptionAlgorithms() 0 4 2
A getSupportedContentEncryptionAlgorithms() 0 4 2
A enableSignedRequestObjectSupport() 0 6 1
A enableRequestObjectReferenceSupport() 0 9 2
A enableEncryptedRequestObjectSupport() 0 12 3
A enableJkuSupport() 0 4 1
A isEncryptedRequestSupportEnabled() 0 4 1
A load() 0 14 3
A createFromRequestParameter() 0 15 3
A createFromRequestUriParameter() 0 16 3
A checkIssuerAndClientId() 0 8 4
A checkRequestUri() 0 19 6
A loadRequestObject() 0 36 5
B tryToLoadEncryptedRequest() 0 23 6
A checkAlgorithms() 0 9 3
A checkUsedAlgorithm() 0 7 2
A downloadContent() 0 15 3
A getClient() 0 9 4
B getClientKeySet() 0 37 9

How to fix   Complexity   

Complex Class

Complex classes like AuthorizationRequestLoader 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 AuthorizationRequestLoader, 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\AuthorizationEndpoint\AuthorizationRequest;
15
16
use Base64Url\Base64Url;
17
use Http\Client\HttpClient;
18
use Jose\Component\Checker\ClaimCheckerManager;
19
use Jose\Component\Core\Converter\StandardConverter;
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\Client\ClientRepository;
30
use OAuth2Framework\Component\Core\Message\OAuth2Error;
31
use Psr\Http\Message\ServerRequestInterface;
32
use Zend\Diactoros\Request;
33
34
class AuthorizationRequestLoader
35
{
36
    /**
37
     * @var ClientRepository
38
     */
39
    private $clientRepository;
40
41
    /**
42
     * @var bool
43
     */
44
    private $requestObjectAllowed = false;
45
46
    /**
47
     * @var bool
48
     */
49
    private $requestObjectReferenceAllowed = false;
50
51
    /**
52
     * @var JWKSet
53
     */
54
    private $keyEncryptionKeySet = null;
55
56
    /**
57
     * @var bool
58
     */
59
    private $requireRequestUriRegistration = true;
60
61
    /**
62
     * @var bool
63
     */
64
    private $requireEncryption = false;
65
66
    /**
67
     * @var HttpClient|null
68
     */
69
    private $client;
70
71
    /**
72
     * @var JWSVerifier
73
     */
74
    private $jwsVerifier;
75
76
    /**
77
     * @var ClaimCheckerManager
78
     */
79
    private $claimCheckerManager;
80
81
    /**
82
     * @var JWELoader
83
     */
84
    private $jweLoader;
85
86
    /**
87
     * @var JKUFactory|null
88
     */
89
    private $jkuFactory;
90
91
    public function __construct(ClientRepository $clientRepository)
92
    {
93
        $this->clientRepository = $clientRepository;
94
    }
95
96
    public function isRequestUriRegistrationRequired(): bool
97
    {
98
        return $this->requireRequestUriRegistration;
99
    }
100
101
    public function isRequestObjectSupportEnabled(): bool
102
    {
103
        return $this->requestObjectAllowed;
104
    }
105
106
    public function isRequestObjectReferenceSupportEnabled(): bool
107
    {
108
        return $this->requestObjectReferenceAllowed;
109
    }
110
111
    /**
112
     * @return string[]
113
     */
114
    public function getSupportedSignatureAlgorithms(): array
115
    {
116
        return null === $this->jwsVerifier ? [] : $this->jwsVerifier->getSignatureAlgorithmManager()->list();
117
    }
118
119
    /**
120
     * @return string[]
121
     */
122
    public function getSupportedKeyEncryptionAlgorithms(): array
123
    {
124
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
125
    }
126
127
    /**
128
     * @return string[]
129
     */
130
    public function getSupportedContentEncryptionAlgorithms(): array
131
    {
132
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
133
    }
134
135
    public function enableSignedRequestObjectSupport(JWSVerifier $jwsVerifier, ClaimCheckerManager $claimCheckerManager)
136
    {
137
        $this->jwsVerifier = $jwsVerifier;
138
        $this->claimCheckerManager = $claimCheckerManager;
139
        $this->requestObjectAllowed = true;
140
    }
141
142
    public function enableRequestObjectReferenceSupport(HttpClient $client, bool $requireRequestUriRegistration)
143
    {
144
        if (!$this->isRequestObjectSupportEnabled()) {
145
            throw new \InvalidArgumentException('Request object support must be enabled first.');
146
        }
147
        $this->requestObjectReferenceAllowed = true;
148
        $this->requireRequestUriRegistration = $requireRequestUriRegistration;
149
        $this->client = $client;
150
    }
151
152
    public function enableEncryptedRequestObjectSupport(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $requireEncryption)
153
    {
154
        if (!$this->isRequestObjectSupportEnabled()) {
155
            throw new \InvalidArgumentException('Request object support must be enabled first.');
156
        }
157
        if (0 === $keyEncryptionKeySet->count()) {
158
            throw new \InvalidArgumentException('The encryption key set must have at least one key.');
159
        }
160
        $this->jweLoader = $jweLoader;
161
        $this->requireEncryption = $requireEncryption;
162
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
163
    }
164
165
    public function enableJkuSupport(JKUFactory $jkuFactory)
166
    {
167
        $this->jkuFactory = $jkuFactory;
168
    }
169
170
    public function isEncryptedRequestSupportEnabled(): bool
171
    {
172
        return null !== $this->keyEncryptionKeySet;
173
    }
174
175
    public function load(ServerRequestInterface $request): AuthorizationRequest
176
    {
177
        $client = null;
178
        $params = $request->getQueryParams();
179
        if (\array_key_exists('request', $params)) {
180
            $params = $this->createFromRequestParameter($params, $client);
181
        } elseif (\array_key_exists('request_uri', $params)) {
182
            $params = $this->createFromRequestUriParameter($params, $client);
183
        } else {
184
            $client = $this->getClient($params);
185
        }
186
187
        return new AuthorizationRequest($client, $params);
0 ignored issues
show
Bug introduced by
It seems like $client defined by null on line 177 can be null; however, OAuth2Framework\Componen...nRequest::__construct() 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...
188
    }
189
190
    private function createFromRequestParameter(array $params, Client &$client = null): array
191
    {
192
        if (false === $this->isRequestObjectSupportEnabled()) {
193
            throw OAuth2Error::requestNotSupported('The parameter "request" is not supported.');
194
        }
195
        $request = $params['request'];
196
        if (!\is_string($request)) {
197
            throw OAuth2Error::requestNotSupported('The parameter "request" must be an assertion.');
198
        }
199
200
        $params = $this->loadRequestObject($params, $request, $client);
201
        $this->checkIssuerAndClientId($params);
202
203
        return $params;
204
    }
205
206
    private function createFromRequestUriParameter(array $params, Client &$client = null): array
207
    {
208
        if (false === $this->isRequestObjectReferenceSupportEnabled()) {
209
            throw OAuth2Error::requestUriNotSupported('The parameter "request_uri" is not supported.');
210
        }
211
        $requestUri = $params['request_uri'];
212
        if (\Safe\preg_match('#/\.\.?(/|$)#', $requestUri)) {
213
            throw OAuth2Error::invalidRequestUri('The request Uri is not allowed.');
214
        }
215
        $content = $this->downloadContent($requestUri);
216
        $params = $this->loadRequestObject($params, $content, $client);
217
        $this->checkRequestUri($client, $requestUri);
0 ignored issues
show
Bug introduced by
It seems like $client defined by parameter $client on line 206 can be null; however, OAuth2Framework\Componen...ader::checkRequestUri() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
218
        $this->checkIssuerAndClientId($params);
219
220
        return $params;
221
    }
222
223
    private function checkIssuerAndClientId(array $params)
224
    {
225
        if (\array_key_exists('iss', $params) && \array_key_exists('client_id', $params)) {
226
            if ($params['iss'] !== $params['client_id']) {
227
                throw new \InvalidArgumentException('The issuer of the request object is not the client who requests the authorization.');
228
            }
229
        }
230
    }
231
232
    private function checkRequestUri(Client $client, $requestUri)
233
    {
234
        $storedRequestUris = $client->has('request_uris') ? $client->get('request_uris') : [];
235
        if (empty($storedRequestUris)) {
236
            if ($this->isRequestUriRegistrationRequired()) {
237
                throw OAuth2Error::invalidRequestUri('The clients shall register at least one request object uri.');
238
            }
239
240
            return;
241
        }
242
243
        foreach ($storedRequestUris as $storedRequestUri) {
244
            if (0 === \strcasecmp(\mb_substr($requestUri, 0, \mb_strlen($storedRequestUri, '8bit'), '8bit'), $storedRequestUri)) {
245
                return;
246
            }
247
        }
248
249
        throw OAuth2Error::invalidRequestUri('The request Uri is not allowed.');
250
    }
251
252
    private function loadRequestObject(array $params, string $request, Client &$client = null): array
253
    {
254
        // FIXME Can be
255
        // - encrypted (not supported)
256
        // - encrypted and signed (supported)
257
        // - signed (supported)
258
        $request = $this->tryToLoadEncryptedRequest($request);
259
260
        try {
261
            $jsonConverter = new StandardConverter();
0 ignored issues
show
Deprecated Code introduced by
The class Jose\Component\Core\Converter\StandardConverter has been deprecated with message: This class is deprecated in v1.3 and will be removed in v2.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
262
            $serializer = new CompactSerializer($jsonConverter);
263
            $jwt = $serializer->unserialize($request);
264
265
            $claims = $jsonConverter->decode(
266
                $jwt->getPayload()
267
            );
268
            if (!\is_array($claims)) {
269
                throw new \InvalidArgumentException('Invalid assertion. The payload must contain claims.');
270
            }
271
            $this->claimCheckerManager->check($claims);
272
            $parameters = \array_merge($params, $claims);
273
            $client = $this->getClient($parameters);
274
275
            $public_key_set = $this->getClientKeySet($client);
276
            $this->checkAlgorithms($jwt, $client);
277
            if (!$this->jwsVerifier->verifyWithKeySet($jwt, $public_key_set, 0)) { //FIXME: header checker should be used
278
                throw new \InvalidArgumentException('The verification of the request object failed.');
279
            }
280
281
            return $parameters;
282
        } catch (OAuth2Error $e) {
283
            throw $e;
284
        } catch (\Exception $e) {
285
            throw OAuth2Error::invalidRequestObject($e->getMessage(), [], $e);
286
        }
287
    }
288
289
    private function tryToLoadEncryptedRequest(string $request): string
290
    {
291
        if (null === $this->jweLoader) {
292
            return $request;
293
        }
294
295
        try {
296
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($request, $this->keyEncryptionKeySet, $recipient);
297
            if (1 !== $jwe->countRecipients()) {
298
                throw new \InvalidArgumentException('The request must use the compact serialization mode.');
299
            }
300
301
            return $jwe->getPayload();
302
        } catch (OAuth2Error $e) {
303
            throw $e;
304
        } catch (\Exception $e) {
305
            if (true === $this->requireEncryption) {
306
                throw OAuth2Error::invalidRequestObject($e->getMessage(), [], $e);
307
            }
308
309
            return $request;
310
        }
311
    }
312
313
    private function checkAlgorithms(JWS $jws, Client $client)
314
    {
315
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
316
        if ($client->has('request_object_signing_alg') && $signatureAlgorithm !== $client->get('request_object_signing_alg')) {
317
            throw new \InvalidArgumentException('Request Object signature algorithm not allowed for the client.');
318
        }
319
320
        $this->checkUsedAlgorithm($signatureAlgorithm);
321
    }
322
323
    private function checkUsedAlgorithm(string $algorithm): void
324
    {
325
        $supportedAlgorithms = $this->getSupportedSignatureAlgorithms();
326
        if (!\in_array($algorithm, $supportedAlgorithms, true)) {
327
            throw new \InvalidArgumentException(\Safe\sprintf('The algorithm "%s" is not allowed for request object signatures. Please use one of the following algorithm(s): %s', $algorithm, \implode(', ', $supportedAlgorithms)));
328
        }
329
    }
330
331
    private function downloadContent(string $url): string
332
    {
333
        $request = new Request($url, 'GET');
334
        $response = $this->client->sendRequest($request);
335
        if (200 !== $response->getStatusCode()) {
336
            throw new \InvalidArgumentException();
337
        }
338
339
        $content = $response->getBody()->getContents();
340
        if (!\is_string($content)) {
341
            throw OAuth2Error::invalidRequestUri('Unable to get content.');
342
        }
343
344
        return $content;
345
    }
346
347
    private function getClient(array $params): Client
348
    {
349
        $client = \array_key_exists('client_id', $params) ? $this->clientRepository->find(new ClientId($params['client_id'])) : null;
350
        if (!$client instanceof Client || true === $client->isDeleted()) {
351
            throw OAuth2Error::invalidRequest('Parameter "client_id" missing or invalid.');
352
        }
353
354
        return $client;
355
    }
356
357
    private function getClientKeySet(Client $client): JWKSet
358
    {
359
        $keyset = JWKSet::createFromKeys([]);
360
        if ($client->has('jwks')) {
361
            $jwks = JWKSet::createFromJson($client->get('jwks'));
362
            foreach ($jwks as $jwk) {
363
                $keyset = $keyset->with($jwk);
364
            }
365
        }
366
        if ($client->has('client_secret')) {
367
            $jwk = JWK::create([
368
                'kty' => 'oct',
369
                'use' => 'sig',
370
                'k' => Base64Url::encode($client->get('client_secret')),
371
            ]);
372
            $keyset = $keyset->with($jwk);
373
        }
374
        if ($client->has('jwks_uri') && null !== $this->jkuFactory) {
375
            $jwks_uri = $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
376
            foreach ($jwks_uri as $jwk) {
377
                $keyset = $keyset->with($jwk);
378
            }
379
        }
380
        if (\in_array('none', $this->getSupportedSignatureAlgorithms(), true)) {
381
            $keyset = $keyset->with(JWK::create([
382
                'kty' => 'none',
383
                'alg' => 'none',
384
                'use' => 'sig',
385
            ]));
386
        }
387
388
        if (0 === $keyset->count()) {
389
            throw new \InvalidArgumentException('The client has no key or key set.');
390
        }
391
392
        return $keyset;
393
    }
394
}
395