Failed Conditions
Push — master ( 25685a...ee6f6b )
by Florent
69:19 queued 42:36
created

AuthorizationRequestLoader   F

Complexity

Total Complexity 68

Size/Duplication

Total Lines 407
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
wmc 68
lcom 1
cbo 21
dl 0
loc 407
rs 2.96
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
A tryToLoadEncryptedRequest() 0 21 5
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;
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\OAuth2Message;
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 null|HttpClient
68
     */
69
    private $client = null;
70
71
    /**
72
     * @var JWSVerifier
73
     */
74
    private $jwsVerifier = null;
75
76
    /**
77
     * @var ClaimCheckerManager
78
     */
79
    private $claimCheckerManager = null;
80
81
    /**
82
     * @var JWELoader
83
     */
84
    private $jweLoader = null;
85
86
    /**
87
     * @var null|JKUFactory
88
     */
89
    private $jkuFactory = null;
90
91
    /**
92
     * AuthorizationRequestLoader constructor.
93
     */
94
    public function __construct(ClientRepository $clientRepository)
95
    {
96
        $this->clientRepository = $clientRepository;
97
    }
98
99
    public function isRequestUriRegistrationRequired(): bool
100
    {
101
        return $this->requireRequestUriRegistration;
102
    }
103
104
    public function isRequestObjectSupportEnabled(): bool
105
    {
106
        return $this->requestObjectAllowed;
107
    }
108
109
    public function isRequestObjectReferenceSupportEnabled(): bool
110
    {
111
        return $this->requestObjectReferenceAllowed;
112
    }
113
114
    /**
115
     * @return string[]
116
     */
117
    public function getSupportedSignatureAlgorithms(): array
118
    {
119
        return null === $this->jwsVerifier ? [] : $this->jwsVerifier->getSignatureAlgorithmManager()->list();
120
    }
121
122
    /**
123
     * @return string[]
124
     */
125
    public function getSupportedKeyEncryptionAlgorithms(): array
126
    {
127
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
128
    }
129
130
    /**
131
     * @return string[]
132
     */
133
    public function getSupportedContentEncryptionAlgorithms(): array
134
    {
135
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
136
    }
137
138
    public function enableSignedRequestObjectSupport(JWSVerifier $jwsVerifier, ClaimCheckerManager $claimCheckerManager)
139
    {
140
        $this->jwsVerifier = $jwsVerifier;
141
        $this->claimCheckerManager = $claimCheckerManager;
142
        $this->requestObjectAllowed = true;
143
    }
144
145
    public function enableRequestObjectReferenceSupport(HttpClient $client, bool $requireRequestUriRegistration)
146
    {
147
        if (!$this->isRequestObjectSupportEnabled()) {
148
            throw new \InvalidArgumentException('Request object support must be enabled first.');
149
        }
150
        $this->requestObjectReferenceAllowed = true;
151
        $this->requireRequestUriRegistration = $requireRequestUriRegistration;
152
        $this->client = $client;
153
    }
154
155
    /**
156
     * @throws \InvalidArgumentException
157
     */
158
    public function enableEncryptedRequestObjectSupport(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $requireEncryption)
159
    {
160
        if (!$this->isRequestObjectSupportEnabled()) {
161
            throw new \InvalidArgumentException('Request object support must be enabled first.');
162
        }
163
        if (0 === $keyEncryptionKeySet->count()) {
164
            throw new \InvalidArgumentException('The encryption key set must have at least one key.');
165
        }
166
        $this->jweLoader = $jweLoader;
167
        $this->requireEncryption = $requireEncryption;
168
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
169
    }
170
171
    public function enableJkuSupport(JKUFactory $jkuFactory)
172
    {
173
        $this->jkuFactory = $jkuFactory;
174
    }
175
176
    public function isEncryptedRequestSupportEnabled(): bool
177
    {
178
        return null !== $this->keyEncryptionKeySet;
179
    }
180
181
    /**
182
     * @throws OAuth2Message
183
     * @throws \Exception
184
     * @throws \Http\Client\Exception
185
     */
186
    public function load(ServerRequestInterface $request): Authorization
187
    {
188
        $client = null;
189
        $params = $request->getQueryParams();
190
        if (\array_key_exists('request', $params)) {
191
            $params = $this->createFromRequestParameter($params, $client);
192
        } elseif (\array_key_exists('request_uri', $params)) {
193
            $params = $this->createFromRequestUriParameter($params, $client);
194
        } else {
195
            $client = $this->getClient($params);
196
        }
197
198
        return new Authorization($client, $params);
0 ignored issues
show
Bug introduced by
It seems like $client defined by null on line 188 can be null; however, OAuth2Framework\Componen...rization::__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...
199
    }
200
201
    /**
202
     * @param Client $client
0 ignored issues
show
Documentation introduced by
Should the type for parameter $client not be null|Client?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
203
     *
204
     * @throws OAuth2Message
205
     */
206
    private function createFromRequestParameter(array $params, Client &$client = null): array
207
    {
208
        if (false === $this->isRequestObjectSupportEnabled()) {
209
            throw new OAuth2Message(400, OAuth2Message::ERROR_REQUEST_NOT_SUPPORTED, 'The parameter "request" is not supported.');
210
        }
211
        $request = $params['request'];
212
        if (!\is_string($request)) {
213
            throw new OAuth2Message(400, OAuth2Message::ERROR_REQUEST_NOT_SUPPORTED, 'The parameter "request" must be an assertion.');
214
        }
215
216
        $params = $this->loadRequestObject($params, $request, $client);
217
        $this->checkIssuerAndClientId($params);
218
219
        return $params;
220
    }
221
222
    /**
223
     * @param Client $client
0 ignored issues
show
Documentation introduced by
Should the type for parameter $client not be null|Client?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
224
     *
225
     * @throws OAuth2Message
226
     * @throws \Exception
227
     * @throws \Http\Client\Exception
228
     */
229
    private function createFromRequestUriParameter(array $params, Client &$client = null): array
230
    {
231
        if (false === $this->isRequestObjectReferenceSupportEnabled()) {
232
            throw new OAuth2Message(400, OAuth2Message::ERROR_REQUEST_URI_NOT_SUPPORTED, 'The parameter "request_uri" is not supported.');
233
        }
234
        $requestUri = $params['request_uri'];
235
        if (\preg_match('#/\.\.?(/|$)#', $requestUri)) {
236
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_URI, 'The request Uri is not allowed.');
237
        }
238
        $content = $this->downloadContent($requestUri);
239
        $params = $this->loadRequestObject($params, $content, $client);
240
        $this->checkRequestUri($client, $requestUri);
0 ignored issues
show
Bug introduced by
It seems like $client defined by parameter $client on line 229 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...
241
        $this->checkIssuerAndClientId($params);
242
243
        return $params;
244
    }
245
246
    /**
247
     * @throws \InvalidArgumentException
248
     */
249
    private function checkIssuerAndClientId(array $params)
250
    {
251
        if (\array_key_exists('iss', $params) && \array_key_exists('client_id', $params)) {
252
            if ($params['iss'] !== $params['client_id']) {
253
                throw new \InvalidArgumentException('The issuer of the request object is not the client who requests the authorization.');
254
            }
255
        }
256
    }
257
258
    /**
259
     * @param string $requestUri
260
     *
261
     * @throws OAuth2Message
262
     */
263
    private function checkRequestUri(Client $client, $requestUri)
264
    {
265
        $storedRequestUris = $client->has('request_uris') ? $client->get('request_uris') : [];
266
        if (empty($storedRequestUris)) {
267
            if ($this->isRequestUriRegistrationRequired()) {
268
                throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_URI, 'The clients shall register at least one request object uri.');
269
            }
270
271
            return;
272
        }
273
274
        foreach ($storedRequestUris as $storedRequestUri) {
275
            if (0 === \strcasecmp(\mb_substr($requestUri, 0, \mb_strlen($storedRequestUri, '8bit'), '8bit'), $storedRequestUri)) {
276
                return;
277
            }
278
        }
279
280
        throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_URI, 'The request Uri is not allowed.');
281
    }
282
283
    /**
284
     * @throws OAuth2Message
285
     */
286
    private function loadRequestObject(array $params, string $request, Client &$client = null): array
287
    {
288
        // FIXME Can be
289
        // - encrypted (not supported)
290
        // - encrypted and signed (supported)
291
        // - signed (supported)
292
        $request = $this->tryToLoadEncryptedRequest($request);
293
294
        try {
295
            $jsonConverter = new StandardConverter();
296
            $serializer = new CompactSerializer($jsonConverter);
297
            $jwt = $serializer->unserialize($request);
298
299
            $claims = $jsonConverter->decode(
300
                $jwt->getPayload()
301
            );
302
            if (!\is_array($claims)) {
303
                throw new \InvalidArgumentException('Invalid assertion. The payload must contain claims.');
304
            }
305
            $this->claimCheckerManager->check($claims);
306
            $parameters = \array_merge($params, $claims);
307
            $client = $this->getClient($parameters);
308
309
            $public_key_set = $this->getClientKeySet($client);
310
            $this->checkAlgorithms($jwt, $client);
311
            if (!$this->jwsVerifier->verifyWithKeySet($jwt, $public_key_set, 0)) { //FIXME: header checker should be used
312
                throw new \InvalidArgumentException('The verification of the request object failed.');
313
            }
314
315
            return $parameters;
316
        } catch (OAuth2Message $e) {
317
            throw $e;
318
        } catch (\Exception $e) {
319
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), [], $e);
320
        }
321
    }
322
323
    /**
324
     * @throws OAuth2Message
325
     */
326
    private function tryToLoadEncryptedRequest(string $request): string
327
    {
328
        if (null === $this->jweLoader) {
329
            return $request;
330
        }
331
332
        try {
333
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($request, $this->keyEncryptionKeySet, $recipient);
334
            if (1 !== $jwe->countRecipients()) {
335
                throw new \InvalidArgumentException('The request must use the compact serialization mode.');
336
            }
337
338
            return $jwe->getPayload();
339
        } catch (\Exception $e) {
340
            if (true === $this->requireEncryption) {
341
                throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), [], $e);
342
            }
343
344
            return $request;
345
        }
346
    }
347
348
    /**
349
     * @throws \InvalidArgumentException
350
     */
351
    private function checkAlgorithms(JWS $jws, Client $client)
352
    {
353
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
354
        if ($client->has('request_object_signing_alg') && $signatureAlgorithm !== $client->get('request_object_signing_alg')) {
355
            throw new \InvalidArgumentException('Request Object signature algorithm not allowed for the client.');
356
        }
357
358
        $this->checkUsedAlgorithm($signatureAlgorithm);
359
    }
360
361
    private function checkUsedAlgorithm(string $algorithm): void
362
    {
363
        $supportedAlgorithms = $this->getSupportedSignatureAlgorithms();
364
        if (!\in_array($algorithm, $supportedAlgorithms, true)) {
365
            throw new \InvalidArgumentException(\sprintf('The algorithm "%s" is not allowed for request object signatures. Please use one of the following algorithm(s): %s', $algorithm, \implode(', ', $supportedAlgorithms)));
366
        }
367
    }
368
369
    /**
370
     * @throws OAuth2Message
371
     * @throws \Exception
372
     * @throws \Http\Client\Exception
373
     */
374
    private function downloadContent(string $url): string
375
    {
376
        $request = new Request($url, 'GET');
377
        $response = $this->client->sendRequest($request);
378
        if (200 !== $response->getStatusCode()) {
379
            throw new \InvalidArgumentException();
380
        }
381
382
        $content = $response->getBody()->getContents();
383
        if (!\is_string($content)) {
384
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_URI, 'Unable to get content.');
385
        }
386
387
        return $content;
388
    }
389
390
    /**
391
     * @throws OAuth2Message
392
     */
393
    private function getClient(array $params): Client
394
    {
395
        $client = \array_key_exists('client_id', $params) ? $this->clientRepository->find(new ClientId($params['client_id'])) : null;
396
        if (!$client instanceof Client || true === $client->isDeleted()) {
397
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, 'Parameter "client_id" missing or invalid.');
398
        }
399
400
        return $client;
401
    }
402
403
    private function getClientKeySet(Client $client): JWKSet
404
    {
405
        $keyset = JWKSet::createFromKeys([]);
406
        if ($client->has('jwks')) {
407
            $jwks = JWKSet::createFromJson($client->get('jwks'));
408
            foreach ($jwks as $jwk) {
409
                $keyset = $keyset->with($jwk);
410
            }
411
        }
412
        if ($client->has('client_secret')) {
413
            $jwk = JWK::create([
414
                'kty' => 'oct',
415
                'use' => 'sig',
416
                'k' => Base64Url::encode($client->get('client_secret')),
417
            ]);
418
            $keyset = $keyset->with($jwk);
419
        }
420
        if ($client->has('jwks_uri') && null !== $this->jkuFactory) {
421
            $jwks_uri = $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
422
            foreach ($jwks_uri as $jwk) {
423
                $keyset = $keyset->with($jwk);
424
            }
425
        }
426
        if (\in_array('none', $this->getSupportedSignatureAlgorithms(), true)) {
427
            $keyset = $keyset->with(JWK::create([
428
                'kty' => 'none',
429
                'alg' => 'none',
430
                'use' => 'sig',
431
            ]));
432
        }
433
434
        if (0 === $keyset->count()) {
435
            throw new \InvalidArgumentException('The client has no key or key set.');
436
        }
437
438
        return $keyset;
439
    }
440
}
441