Failed Conditions
Push — master ( 2eee97...42d85f )
by Florent
09:02
created

AuthorizationRequestLoader   C

Complexity

Total Complexity 47

Size/Duplication

Total Lines 413
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
wmc 47
lcom 1
cbo 16
dl 0
loc 413
rs 6.6654
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 enableRequestObjectSupport() 0 8 1
A enableRequestObjectReferenceSupport() 0 7 1
A enableEncryptedRequestObjectSupport() 0 8 1
A isEncryptedRequestsSupportEnabled() 0 4 1
A loadParametersFromRequest() 0 16 3
A createFromRequestParameter() 0 15 2
A createFromStandardRequest() 0 6 1
A createFromRequestUriParameter() 0 18 3
A checkIssuerAndClientId() 0 6 3
A checkRequestUri() 0 13 3
A checkRequestUriPathTraversal() 0 6 2
A getClientRequestUris() 0 8 3
A loadRequest() 0 21 2
A tryToLoadEncryptedRequest() 0 19 4
A checkAlgorithms() 0 5 1
A downloadContent() 0 13 2
A getClient() 0 9 4

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-2017 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\Server\Endpoint\Authorization;
15
16
use Assert\Assertion;
17
use Http\Client\HttpClient;
18
use Jose\Component\Checker\ClaimCheckerManager;
19
use Jose\Component\Core\JWKSet;
20
use Jose\Component\Encryption\JWELoader;
21
use Jose\Component\Signature\JWS;
22
use Jose\Component\Signature\JWSLoader;
23
use OAuth2Framework\Component\Server\Model\Client\Client;
24
use OAuth2Framework\Component\Server\Model\Client\ClientId;
25
use OAuth2Framework\Component\Server\Model\Client\ClientRepositoryInterface;
26
use OAuth2Framework\Component\Server\Response\OAuth2Exception;
27
use OAuth2Framework\Component\Server\Response\OAuth2ResponseFactoryManager;
28
use OAuth2Framework\Component\Server\Util\Uri;
29
use Psr\Http\Message\ServerRequestInterface;
30
use Zend\Diactoros\Request;
31
32
final class AuthorizationRequestLoader
33
{
34
    /**
35
     * @var ClientRepositoryInterface
36
     */
37
    private $clientRepository;
38
39
    /**
40
     * @var bool
41
     */
42
    private $requestObjectAllowed = false;
43
44
    /**
45
     * @var bool
46
     */
47
    private $requestObjectReferenceAllowed = false;
48
49
    /**
50
     * @var JWKSet
51
     */
52
    private $keyEncryptionKeySet = null;
53
54
    /**
55
     * @var bool
56
     */
57
    private $requireRequestUriRegistration = true;
58
59
    /**
60
     * @var bool
61
     */
62
    private $requireEncryption = false;
63
64
    /**
65
     * @var string[]
66
     */
67
    private $mandatoryClaims = [];
68
69
    /**
70
     * @var null|HttpClient
71
     */
72
    private $client = null;
73
74
    /**
75
     * @var JWSLoader
76
     */
77
    private $jwsLoader = null;
78
79
    /**
80
     * @var ClaimCheckerManager
81
     */
82
    private $claimCheckerManager = null;
83
84
    /**
85
     * @var JWELoader
86
     */
87
    private $jweLoader = null;
88
89
    /**
90
     * AuthorizationRequestLoader constructor.
91
     *
92
     * @param ClientRepositoryInterface $clientRepository
93
     */
94
    public function __construct(ClientRepositoryInterface $clientRepository)
95
    {
96
        $this->clientRepository = $clientRepository;
97
    }
98
99
    /**
100
     * @return bool
101
     */
102
    public function isRequestUriRegistrationRequired(): bool
103
    {
104
        return $this->requireRequestUriRegistration;
105
    }
106
107
    /**
108
     * @return bool
109
     */
110
    public function isRequestObjectSupportEnabled(): bool
111
    {
112
        return $this->requestObjectAllowed;
113
    }
114
115
    /**
116
     * @return bool
117
     */
118
    public function isRequestObjectReferenceSupportEnabled(): bool
119
    {
120
        return $this->requestObjectReferenceAllowed;
121
    }
122
123
    /**
124
     * @return string[]
125
     */
126
    public function getSupportedSignatureAlgorithms(): array
127
    {
128
        return null === $this->jwsLoader ? [] : $this->jwsLoader->getSignatureAlgorithmManager()->list();
129
    }
130
131
    /**
132
     * @return string[]
133
     */
134
    public function getSupportedKeyEncryptionAlgorithms(): array
135
    {
136
        return null === $this->jweLoader ? [] : $this->jweLoader->getKeyEncryptionAlgorithmManager()->list();
137
    }
138
139
    /**
140
     * @return string[]
141
     */
142
    public function getSupportedContentEncryptionAlgorithms(): array
143
    {
144
        return null === $this->jweLoader ? [] : $this->jweLoader->getContentEncryptionAlgorithmManager()->list();
145
    }
146
147
    /**
148
     * @param JWSLoader           $jwsLoader
149
     * @param ClaimCheckerManager $claimCheckerManager
150
     * @param string[]            $mandatoryClaims
151
     */
152
    public function enableRequestObjectSupport(JWSLoader $jwsLoader, ClaimCheckerManager $claimCheckerManager, array $mandatoryClaims = [])
153
    {
154
        Assertion::allString($mandatoryClaims, 'The mandatory claims array should contain only claims.');
155
        $this->jwsLoader = $jwsLoader;
156
        $this->claimCheckerManager = $claimCheckerManager;
157
        $this->requestObjectAllowed = true;
158
        $this->mandatoryClaims = $mandatoryClaims;
159
    }
160
161
    /**
162
     * @param HttpClient $client
163
     * @param bool       $requireRequestUriRegistration
164
     */
165
    public function enableRequestObjectReferenceSupport(HttpClient $client, bool $requireRequestUriRegistration)
166
    {
167
        Assertion::true($this->isRequestObjectSupportEnabled(), 'Request object support must be enabled first.');
168
        $this->requestObjectReferenceAllowed = true;
169
        $this->requireRequestUriRegistration = $requireRequestUriRegistration;
170
        $this->client = $client;
171
    }
172
173
    /**
174
     * @param JWELoader $jweLoader
175
     * @param JWKSet    $keyEncryptionKeySet
176
     * @param bool      $requireEncryption
177
     */
178
    public function enableEncryptedRequestObjectSupport(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $requireEncryption)
179
    {
180
        Assertion::true($this->isRequestObjectSupportEnabled(), 'Request object support must be enabled first.');
181
        Assertion::greaterThan($keyEncryptionKeySet->count(), 0, 'The encryption key set must have at least one key.');
182
        $this->jweLoader = $jweLoader;
183
        $this->requireEncryption = $requireEncryption;
184
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
185
    }
186
187
    /**
188
     * @return bool
189
     */
190
    public function isEncryptedRequestsSupportEnabled(): bool
191
    {
192
        return null !== $this->keyEncryptionKeySet;
193
    }
194
195
    /**
196
     * @param ServerRequestInterface $request
197
     *
198
     * @return array
199
     */
200
    public function loadParametersFromRequest(ServerRequestInterface $request): array
201
    {
202
        $params = $request->getQueryParams();
203
        if (array_key_exists('request', $params)) {
204
            $params = $this->createFromRequestParameter($params);
205
        } elseif (array_key_exists('request_uri', $params)) {
206
            $params = $this->createFromRequestUriParameter($params);
207
        } else {
208
            $params = $this->createFromStandardRequest($params);
209
        }
210
211
        $client = $params['client'];
212
        unset($params['client']);
213
214
        return [$client, $params];
215
    }
216
217
    /**
218
     * @param array $params
219
     *
220
     * @throws OAuth2Exception
221
     *
222
     * @return array
223
     */
224
    private function createFromRequestParameter(array $params): array
225
    {
226
        if (false === $this->isRequestObjectSupportEnabled()) {
227
            throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_REQUEST_NOT_SUPPORTED, 'error_description' => 'The parameter \'request\' is not supported.']);
228
        }
229
        $request = $params['request'];
230
        Assertion::string($request);
231
232
        $jws = $this->loadRequest($params, $request, $client);
233
        $claims = json_decode($jws->getPayload(), true);
234
        $params = array_merge($params, $claims, ['client' => $client]);
235
        $this->checkIssuerAndClientId($params);
236
237
        return $params;
238
    }
239
240
    /**
241
     * @param array $params
242
     *
243
     * @return array
244
     */
245
    private function createFromStandardRequest(array $params): array
246
    {
247
        $client = $this->getClient($params);
248
249
        return array_merge($params, ['client' => $client]);
250
    }
251
252
    /**
253
     * @param array $params
254
     *
255
     * @throws OAuth2Exception
256
     *
257
     * @return array
258
     */
259
    private function createFromRequestUriParameter(array $params): array
260
    {
261
        if (false === $this->isRequestObjectReferenceSupportEnabled()) {
262
            throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_REQUEST_URI_NOT_SUPPORTED, 'error_description' => 'The parameter \'request_uri\' is not supported.']);
263
        }
264
        $requestUri = $params['request_uri'];
265
266
        $content = $this->downloadContent($requestUri);
267
        $jws = $this->loadRequest($params, $content, $client);
268
        if (true === $this->isRequestUriRegistrationRequired()) {
269
            $this->checkRequestUri($client, $requestUri);
0 ignored issues
show
Bug introduced by
It seems like $client can be null; however, checkRequestUri() 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...
270
        }
271
        $claims = json_decode($jws->getPayload(), true);
272
        $params = array_merge($params, $claims, ['client' => $client]);
273
        $this->checkIssuerAndClientId($params);
274
275
        return $params;
276
    }
277
278
    /**
279
     * @param array $params
280
     *
281
     * @throws OAuth2Exception
282
     */
283
    private function checkIssuerAndClientId(array $params)
284
    {
285
        if (array_key_exists('iss', $params) && array_key_exists('client_id', $params)) {
286
            Assertion::eq($params['iss'], $params['client_id'], 'The issuer of the request object is not the client who requests the authorization.');
287
        }
288
    }
289
290
    /**
291
     * @param Client $client
292
     * @param string $requestUri
293
     *
294
     * @throws OAuth2Exception
295
     */
296
    private function checkRequestUri(Client $client, $requestUri)
297
    {
298
        $this->checkRequestUriPathTraversal($requestUri);
299
        $stored_request_uris = $this->getClientRequestUris($client);
300
301
        foreach ($stored_request_uris as $stored_request_uri) {
302
            if (strcasecmp(mb_substr($requestUri, 0, mb_strlen($stored_request_uri, '8bit'), '8bit'), $stored_request_uri) === 0) {
303
                return;
304
            }
305
        }
306
307
        throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_REQUEST_URI, 'error_description' => 'The request Uri is not allowed.']);
308
    }
309
310
    /**
311
     * @param string $requestUri
312
     *
313
     * @throws OAuth2Exception
314
     */
315
    private function checkRequestUriPathTraversal($requestUri)
316
    {
317
        if (false === Uri::checkUrl($requestUri, false)) {
318
            throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_CLIENT, 'error_description' => 'The request Uri must not contain path traversal.']);
319
        }
320
    }
321
322
    /**
323
     * @param Client $client
324
     *
325
     * @throws OAuth2Exception
326
     *
327
     * @return string[]
328
     */
329
    private function getClientRequestUris(Client $client): array
330
    {
331
        if (false === $client->has('request_uris') || empty($requestUris = $client->get('request_uris'))) {
332
            throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_CLIENT, 'error_description' => 'The client must register at least one request Uri.']);
333
        }
334
335
        return $requestUris;
336
    }
337
338
    /**
339
     * @param array       $params
340
     * @param string      $request
341
     * @param Client|null $client
342
     *
343
     * @throws OAuth2Exception
344
     *
345
     * @return JWS
346
     */
347
    private function loadRequest(array $params, string $request, Client &$client = null): JWS
348
    {
349
        $request = $this->tryToLoadEncryptedRequest($request);
350
        try {
351
            $jwt = $this->jwsLoader->load($request);
352
            $this->claimCheckerManager->check($jwt);
353
            $claims = json_decode($jwt->getPayload(), true);
354
355
            $client = $this->getClient(array_merge($params, $claims));
356
            $public_key_set = $client->getPublicKeySet();
357
            Assertion::notNull($public_key_set, 'The client does not have signature capabilities.');
358
            $index = $this->jwsLoader->verifyWithKeySet($jwt, $public_key_set);
359
            $this->checkAlgorithms($jwt, $index, $client);
360
            $missing_claims = array_keys(array_diff_key(array_flip($this->mandatoryClaims), $claims));
361
            Assertion::true(0 === count($missing_claims), 'The following mandatory claims are missing: %s.', implode(', ', $missing_claims));
362
        } catch (\Exception $e) {
363
            throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_REQUEST_OBJECT, 'error_description' => $e->getMessage()]);
364
        }
365
366
        return $jwt;
367
    }
368
369
    /**
370
     * @param string $request
371
     *
372
     * @return string
373
     *
374
     * @throws OAuth2Exception
375
     */
376
    private function tryToLoadEncryptedRequest(string $request): string
377
    {
378
        if (null === $this->jweLoader) {
379
            return $request;
380
        }
381
382
        try {
383
            $jwe = $this->jweLoader->load($request);
384
            $jwe = $this->jweLoader->decryptUsingKeySet($jwe, $this->keyEncryptionKeySet);
385
386
            return $jwe->getPayload();
387
        } catch (\Exception $e) {
388
            if (true === $this->requireEncryption) {
389
                throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_REQUEST_OBJECT, 'error_description' => $e->getMessage()]);
390
            }
391
392
            return $request;
393
        }
394
    }
395
396
    /**
397
     * @param JWS    $jwt
398
     * @param int    $index
399
     * @param Client $client
400
     */
401
    private function checkAlgorithms(JWS $jwt, int $index, Client $client)
402
    {
403
        Assertion::true($client->has('request_object_signing_alg'), 'Request Object signature algorithm not defined for the client.');
404
        Assertion::eq($jwt->getSignature($index)->getProtectedHeader('alg'), $client->get('request_object_signing_alg'), 'Request Object signature algorithm not supported by the client.');
405
    }
406
407
    /**
408
     * @param string $url
409
     *
410
     * @throws OAuth2Exception
411
     *
412
     * @return string
413
     */
414
    private function downloadContent($url): string
415
    {
416
        $request = new Request($url, 'GET');
417
        $response = $this->client->sendRequest($request);
418
        Assertion::eq(200, $response->getStatusCode());
419
420
        $content = $response->getBody()->getContents();
421
        if (!is_string($content)) {
422
            throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_REQUEST_URI, 'error_description' => 'Unable to get content.']);
423
        }
424
425
        return $content;
426
    }
427
428
    /**
429
     * @param array $params
430
     *
431
     * @throws OAuth2Exception
432
     *
433
     * @return Client
434
     */
435
    private function getClient(array $params): Client
436
    {
437
        $client = array_key_exists('client_id', $params) ? $this->clientRepository->find(ClientId::create($params['client_id'])) : null;
438
        if (!$client instanceof Client || true === $client->isDeleted()) {
439
            throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_REQUEST, 'error_description' => 'Parameter \'client_id\' missing or invalid.']);
440
        }
441
442
        return $client;
443
    }
444
}
445