Failed Conditions
Push — master ( b5a0b4...819484 )
by Florent
08:00
created

AuthorizationRequestLoader   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 151
dl 0
loc 359
rs 2.88
c 0
b 0
f 0
wmc 69

24 Methods

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

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.

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 can also be of type null; however, parameter $client of OAuth2Framework\Componen...nRequest::__construct() does only seem to accept OAuth2Framework\Component\Core\Client\Client, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

187
        return new AuthorizationRequest(/** @scrutinizer ignore-type */ $client, $params);
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 can also be of type null; however, parameter $client of OAuth2Framework\Componen...ader::checkRequestUri() does only seem to accept OAuth2Framework\Component\Core\Client\Client, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

217
        $this->checkRequestUri(/** @scrutinizer ignore-type */ $client, $requestUri);
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: This class is deprecated in v1.3 and will be removed in v2.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

261
            $jsonConverter = /** @scrutinizer ignore-deprecated */ new StandardConverter();
Loading history...
262
            $serializer = new CompactSerializer($jsonConverter);
263
            $jwt = $serializer->unserialize($request);
264
265
            $claims = $jsonConverter->decode(
266
                $jwt->getPayload()
0 ignored issues
show
Bug introduced by
It seems like $jwt->getPayload() can also be of type null; however, parameter $payload of Jose\Component\Core\Conv...dardConverter::decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

266
                /** @scrutinizer ignore-type */ $jwt->getPayload()
Loading history...
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();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $jwe->getPayload() could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $signatureAlgorithm can also be of type null; however, parameter $algorithm of OAuth2Framework\Componen...r::checkUsedAlgorithm() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

320
        $this->checkUsedAlgorithm(/** @scrutinizer ignore-type */ $signatureAlgorithm);
Loading history...
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);
0 ignored issues
show
Bug introduced by
The method sendRequest() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

334
        /** @scrutinizer ignore-call */ 
335
        $response = $this->client->sendRequest($request);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
335
        if (200 !== $response->getStatusCode()) {
336
            throw new \InvalidArgumentException();
337
        }
338
339
        $content = $response->getBody()->getContents();
340
        if (!\is_string($content)) {
0 ignored issues
show
introduced by
The condition is_string($content) is always true.
Loading history...
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'));
0 ignored issues
show
Bug introduced by
It seems like $client->get('jwks') can also be of type null; however, parameter $json of Jose\Component\Core\JWKSet::createFromJson() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

361
            $jwks = JWKSet::createFromJson(/** @scrutinizer ignore-type */ $client->get('jwks'));
Loading history...
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')),
0 ignored issues
show
Bug introduced by
It seems like $client->get('client_secret') can also be of type null; however, parameter $data of Base64Url\Base64Url::encode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

370
                'k' => Base64Url::encode(/** @scrutinizer ignore-type */ $client->get('client_secret')),
Loading history...
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'));
0 ignored issues
show
Bug introduced by
It seems like $client->get('jwks_uri') can also be of type null; however, parameter $url of Jose\Component\KeyManage...UFactory::loadFromUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

375
            $jwks_uri = $this->jkuFactory->loadFromUrl(/** @scrutinizer ignore-type */ $client->get('jwks_uri'));
Loading history...
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