Failed Conditions
Push — master ( 3bf08d...6e3f66 )
by Florent
03:16
created

AuthorizationRequestLoader   C

Complexity

Total Complexity 47

Size/Duplication

Total Lines 414
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
wmc 47
lcom 1
cbo 16
dl 0
loc 414
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 22 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
351
        try {
352
            $jwt = $this->jwsLoader->load($request);
353
            $this->claimCheckerManager->check($jwt);
354
            $claims = json_decode($jwt->getPayload(), true);
355
356
            $client = $this->getClient(array_merge($params, $claims));
357
            $public_key_set = $client->getPublicKeySet();
358
            Assertion::notNull($public_key_set, 'The client does not have signature capabilities.');
359
            $index = $this->jwsLoader->verifyWithKeySet($jwt, $public_key_set);
360
            $this->checkAlgorithms($jwt, $index, $client);
361
            $missing_claims = array_keys(array_diff_key(array_flip($this->mandatoryClaims), $claims));
362
            Assertion::true(0 === count($missing_claims), 'The following mandatory claims are missing: %s.', implode(', ', $missing_claims));
363
        } catch (\Exception $e) {
364
            throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_REQUEST_OBJECT, 'error_description' => $e->getMessage()]);
365
        }
366
367
        return $jwt;
368
    }
369
370
    /**
371
     * @param string $request
372
     *
373
     * @return string
374
     *
375
     * @throws OAuth2Exception
376
     */
377
    private function tryToLoadEncryptedRequest(string $request): string
378
    {
379
        if (null === $this->jweLoader) {
380
            return $request;
381
        }
382
383
        try {
384
            $jwe = $this->jweLoader->load($request);
385
            $jwe = $this->jweLoader->decryptUsingKeySet($jwe, $this->keyEncryptionKeySet);
386
387
            return $jwe->getPayload();
388
        } catch (\Exception $e) {
389
            if (true === $this->requireEncryption) {
390
                throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_REQUEST_OBJECT, 'error_description' => $e->getMessage()]);
391
            }
392
393
            return $request;
394
        }
395
    }
396
397
    /**
398
     * @param JWS    $jwt
399
     * @param int    $index
400
     * @param Client $client
401
     */
402
    private function checkAlgorithms(JWS $jwt, int $index, Client $client)
403
    {
404
        Assertion::true($client->has('request_object_signing_alg'), 'Request Object signature algorithm not defined for the client.');
405
        Assertion::eq($jwt->getSignature($index)->getProtectedHeader('alg'), $client->get('request_object_signing_alg'), 'Request Object signature algorithm not supported by the client.');
406
    }
407
408
    /**
409
     * @param string $url
410
     *
411
     * @throws OAuth2Exception
412
     *
413
     * @return string
414
     */
415
    private function downloadContent($url): string
416
    {
417
        $request = new Request($url, 'GET');
418
        $response = $this->client->sendRequest($request);
419
        Assertion::eq(200, $response->getStatusCode());
420
421
        $content = $response->getBody()->getContents();
422
        if (!is_string($content)) {
423
            throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_REQUEST_URI, 'error_description' => 'Unable to get content.']);
424
        }
425
426
        return $content;
427
    }
428
429
    /**
430
     * @param array $params
431
     *
432
     * @throws OAuth2Exception
433
     *
434
     * @return Client
435
     */
436
    private function getClient(array $params): Client
437
    {
438
        $client = array_key_exists('client_id', $params) ? $this->clientRepository->find(ClientId::create($params['client_id'])) : null;
439
        if (!$client instanceof Client || true === $client->isDeleted()) {
440
            throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_REQUEST, 'error_description' => 'Parameter \'client_id\' missing or invalid.']);
441
        }
442
443
        return $client;
444
    }
445
}
446