Failed Conditions
Push — master ( 47f59b...bc2c08 )
by Florent
04:48
created

AuthorizationRequestLoader   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 362
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 57
eloc 138
dl 0
loc 362
rs 5.04
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A isRequestObjectReferenceSupportEnabled() 0 3 1
A getSupportedContentEncryptionAlgorithms() 0 3 2
A isRequestObjectSupportEnabled() 0 3 1
A isEncryptedRequestSupportEnabled() 0 3 1
A enableJkuSupport() 0 3 1
A load() 0 12 3
A __construct() 0 3 1
A enableEncryptedRequestObjectSupport() 0 7 1
A createFromRequestUriParameter() 0 15 3
A enableRequestObjectReferenceSupport() 0 7 1
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
A checkUsedAlgorithm() 0 4 1
A checkRequestUri() 0 18 6
A checkIssuerAndClientId() 0 4 3
A downloadContent() 0 7 1
A tryToLoadEncryptedRequest() 0 19 5
A checkAlgorithms() 0 9 2
A getClient() 0 8 4
B getClientKeySet() 0 34 8
A loadRequestObject() 0 29 3

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-2019 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 Assert\Assertion;
17
use Base64Url\Base64Url;
18
use Jose\Component\Checker\ClaimCheckerManager;
19
use Jose\Component\Core\JWK;
20
use Jose\Component\Core\JWKSet;
21
use Jose\Component\Core\Util\JsonConverter;
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\Client\ClientInterface;
32
use Psr\Http\Message\RequestFactoryInterface;
33
use Safe\Exceptions\PcreException;
34
use function Safe\preg_match;
35
use function Safe\sprintf;
36
37
class AuthorizationRequestLoader
38
{
39
    /**
40
     * @var ClientRepository
41
     */
42
    private $clientRepository;
43
44
    /**
45
     * @var bool
46
     */
47
    private $requestObjectAllowed = false;
48
49
    /**
50
     * @var bool
51
     */
52
    private $requestObjectReferenceAllowed = false;
53
54
    /**
55
     * @var JWKSet
56
     */
57
    private $keyEncryptionKeySet;
58
59
    /**
60
     * @var bool
61
     */
62
    private $requireRequestUriRegistration = true;
63
64
    /**
65
     * @var bool
66
     */
67
    private $requireEncryption = false;
68
69
    /**
70
     * @var null|ClientInterface
71
     */
72
    private $client;
73
74
    /**
75
     * @var JWSVerifier
76
     */
77
    private $jwsVerifier;
78
79
    /**
80
     * @var ClaimCheckerManager
81
     */
82
    private $claimCheckerManager;
83
84
    /**
85
     * @var null|JWELoader
86
     */
87
    private $jweLoader;
88
89
    /**
90
     * @var null|JKUFactory
91
     */
92
    private $jkuFactory;
93
94
    /**
95
     * @var RequestFactoryInterface
96
     */
97
    private $requestFactory;
98
99
    public function __construct(ClientRepository $clientRepository)
100
    {
101
        $this->clientRepository = $clientRepository;
102
    }
103
104
    public function isRequestUriRegistrationRequired(): bool
105
    {
106
        return $this->requireRequestUriRegistration;
107
    }
108
109
    public function isRequestObjectSupportEnabled(): bool
110
    {
111
        return $this->requestObjectAllowed;
112
    }
113
114
    public function isRequestObjectReferenceSupportEnabled(): bool
115
    {
116
        return $this->requestObjectReferenceAllowed;
117
    }
118
119
    /**
120
     * @return string[]
121
     */
122
    public function getSupportedSignatureAlgorithms(): array
123
    {
124
        return null === $this->jwsVerifier ? [] : $this->jwsVerifier->getSignatureAlgorithmManager()->list();
125
    }
126
127
    /**
128
     * @return string[]
129
     */
130
    public function getSupportedKeyEncryptionAlgorithms(): array
131
    {
132
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
133
    }
134
135
    /**
136
     * @return string[]
137
     */
138
    public function getSupportedContentEncryptionAlgorithms(): array
139
    {
140
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
141
    }
142
143
    public function enableSignedRequestObjectSupport(JWSVerifier $jwsVerifier, ClaimCheckerManager $claimCheckerManager): void
144
    {
145
        $this->jwsVerifier = $jwsVerifier;
146
        $this->claimCheckerManager = $claimCheckerManager;
147
        $this->requestObjectAllowed = true;
148
    }
149
150
    public function enableRequestObjectReferenceSupport(ClientInterface $client, RequestFactoryInterface $requestFactory, bool $requireRequestUriRegistration): void
151
    {
152
        Assertion::true($this->isRequestObjectSupportEnabled(), 'Request object support must be enabled first.');
153
        $this->requestObjectReferenceAllowed = true;
154
        $this->requireRequestUriRegistration = $requireRequestUriRegistration;
155
        $this->requestFactory = $requestFactory;
156
        $this->client = $client;
157
    }
158
159
    public function enableEncryptedRequestObjectSupport(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $requireEncryption): void
160
    {
161
        Assertion::true($this->isRequestObjectSupportEnabled(), 'Request object support must be enabled first.');
162
        Assertion::greaterThan($keyEncryptionKeySet->count(), 0, 'The encryption key set must have at least one key.');
163
        $this->jweLoader = $jweLoader;
164
        $this->requireEncryption = $requireEncryption;
165
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
166
    }
167
168
    public function enableJkuSupport(JKUFactory $jkuFactory): void
169
    {
170
        $this->jkuFactory = $jkuFactory;
171
    }
172
173
    public function isEncryptedRequestSupportEnabled(): bool
174
    {
175
        return null !== $this->keyEncryptionKeySet;
176
    }
177
178
    /**
179
     * @throws OAuth2Error
180
     * @throws PcreException
181
     */
182
    public function load(array $queryParameters): AuthorizationRequest
183
    {
184
        $client = null;
185
        if (\array_key_exists('request', $queryParameters)) {
186
            $queryParameters = $this->createFromRequestParameter($queryParameters, $client);
187
        } elseif (\array_key_exists('request_uri', $queryParameters)) {
188
            $queryParameters = $this->createFromRequestUriParameter($queryParameters, $client);
189
        } else {
190
            $client = $this->getClient($queryParameters);
191
        }
192
193
        return new AuthorizationRequest($client, $queryParameters);
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

193
        return new AuthorizationRequest(/** @scrutinizer ignore-type */ $client, $queryParameters);
Loading history...
194
    }
195
196
    /**
197
     * @throws OAuth2Error
198
     */
199
    private function createFromRequestParameter(array $params, Client &$client = null): array
200
    {
201
        if (false === $this->isRequestObjectSupportEnabled()) {
202
            throw OAuth2Error::requestNotSupported('The parameter "request" is not supported.');
203
        }
204
        $request = $params['request'];
205
        if (!\is_string($request)) {
206
            throw OAuth2Error::requestNotSupported('The parameter "request" must be an assertion.');
207
        }
208
209
        $params = $this->loadRequestObject($params, $request, $client);
210
        $this->checkIssuerAndClientId($params);
211
212
        return $params;
213
    }
214
215
    /**
216
     * @throws OAuth2Error
217
     * @throws PcreException
218
     */
219
    private function createFromRequestUriParameter(array $params, Client &$client = null): array
220
    {
221
        if (false === $this->isRequestObjectReferenceSupportEnabled()) {
222
            throw OAuth2Error::requestUriNotSupported('The parameter "request_uri" is not supported.');
223
        }
224
        $requestUri = $params['request_uri'];
225
        if (1 === preg_match('#/\.\.?(/|$)#', $requestUri)) {
226
            throw OAuth2Error::invalidRequestUri('The request Uri is not allowed.');
227
        }
228
        $content = $this->downloadContent($requestUri);
229
        $params = $this->loadRequestObject($params, $content, $client);
230
        $this->checkRequestUri($client, $requestUri);
231
        $this->checkIssuerAndClientId($params);
232
233
        return $params;
234
    }
235
236
    private function checkIssuerAndClientId(array $params): void
237
    {
238
        if (\array_key_exists('iss', $params) && \array_key_exists('client_id', $params)) {
239
            Assertion::eq($params['iss'], $params['client_id'], 'The issuer of the request object is not the client who requests the authorization.');
240
        }
241
    }
242
243
    /**
244
     * @throws OAuth2Error
245
     */
246
    private function checkRequestUri(Client $client, string $requestUri): void
247
    {
248
        $storedRequestUris = $client->has('request_uris') ? $client->get('request_uris') : [];
249
        if (empty($storedRequestUris)) {
250
            if ($this->isRequestUriRegistrationRequired()) {
251
                throw OAuth2Error::invalidRequestUri('The clients shall register at least one request object uri.');
252
            }
253
254
            return;
255
        }
256
257
        foreach ($storedRequestUris as $storedRequestUri) {
258
            if (0 === strcasecmp(mb_substr($requestUri, 0, mb_strlen($storedRequestUri, '8bit'), '8bit'), $storedRequestUri)) {
259
                return;
260
            }
261
        }
262
263
        throw OAuth2Error::invalidRequestUri('The request Uri is not allowed.');
264
    }
265
266
    /**
267
     * @throws OAuth2Error
268
     */
269
    private function loadRequestObject(array $params, string $request, Client &$client = null): array
270
    {
271
        // FIXME Can be
272
        // - encrypted (not supported)
273
        // - encrypted and signed (supported)
274
        // - signed (supported)
275
        $request = $this->tryToLoadEncryptedRequest($request);
276
277
        try {
278
            $serializer = new CompactSerializer();
279
            $jwt = $serializer->unserialize($request);
280
281
            $claims = JsonConverter::decode(
282
                $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\Util\JsonConverter::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

282
                /** @scrutinizer ignore-type */ $jwt->getPayload()
Loading history...
283
            );
284
            Assertion::isArray($claims, 'Invalid assertion. The payload must contain claims.');
285
            $this->claimCheckerManager->check($claims);
286
            $parameters = array_merge($params, $claims);
287
            $client = $this->getClient($parameters);
288
289
            $public_key_set = $this->getClientKeySet($client);
290
            $this->checkAlgorithms($jwt, $client);
291
            Assertion::true($this->jwsVerifier->verifyWithKeySet($jwt, $public_key_set, 0), 'The verification of the request object failed.'); //FIXME: header checker should be used
292
293
            return $parameters;
294
        } catch (OAuth2Error $e) {
295
            throw $e;
296
        } catch (\Throwable $e) {
297
            throw OAuth2Error::invalidRequestObject($e->getMessage(), [], $e);
298
        }
299
    }
300
301
    /**
302
     * @throws OAuth2Error
303
     */
304
    private function tryToLoadEncryptedRequest(string $request): string
305
    {
306
        if (null === $this->jweLoader) {
307
            return $request;
308
        }
309
310
        try {
311
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($request, $this->keyEncryptionKeySet, $recipient);
312
            Assertion::eq(1, $jwe->countRecipients(), 'The request must use the compact serialization mode.');
313
314
            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...
315
        } catch (OAuth2Error $e) {
316
            throw $e;
317
        } catch (\Throwable $e) {
318
            if (true === $this->requireEncryption) {
319
                throw OAuth2Error::invalidRequestObject($e->getMessage(), [], $e);
320
            }
321
322
            return $request;
323
        }
324
    }
325
326
    private function checkAlgorithms(JWS $jws, Client $client): void
327
    {
328
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
329
        Assertion::string($signatureAlgorithm, 'Invalid algorithm parameter in Request Object.');
330
        if ($client->has('request_object_signing_alg')) {
331
            Assertion::eq($signatureAlgorithm, $client->get('request_object_signing_alg'), 'Request Object signature algorithm not allowed for the client.');
332
        }
333
334
        $this->checkUsedAlgorithm($signatureAlgorithm);
335
    }
336
337
    private function checkUsedAlgorithm(string $algorithm): void
338
    {
339
        $supportedAlgorithms = $this->getSupportedSignatureAlgorithms();
340
        Assertion::inArray($algorithm, $supportedAlgorithms, sprintf('The algorithm "%s" is not allowed for request object signatures. Please use one of the following algorithm(s): %s', $algorithm, implode(', ', $supportedAlgorithms)));
341
    }
342
343
    private function downloadContent(string $url): string
344
    {
345
        $request = $this->requestFactory->createRequest('GET', $url);
346
        $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

346
        /** @scrutinizer ignore-call */ 
347
        $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...
347
        Assertion::eq(200, $response->getStatusCode(), 'Unable to load the request object');
348
349
        return $response->getBody()->getContents();
350
    }
351
352
    /**
353
     * @throws OAuth2Error
354
     */
355
    private function getClient(array $params): Client
356
    {
357
        $client = \array_key_exists('client_id', $params) ? $this->clientRepository->find(new ClientId($params['client_id'])) : null;
358
        if (!$client instanceof Client || true === $client->isDeleted()) {
359
            throw OAuth2Error::invalidRequest('Parameter "client_id" missing or invalid.');
360
        }
361
362
        return $client;
363
    }
364
365
    private function getClientKeySet(Client $client): JWKSet
366
    {
367
        $keyset = new JWKSet([]);
368
        if ($client->has('jwks')) {
369
            $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

369
            $jwks = JWKSet::createFromJson(/** @scrutinizer ignore-type */ $client->get('jwks'));
Loading history...
370
            foreach ($jwks as $jwk) {
371
                $keyset = $keyset->with($jwk);
372
            }
373
        }
374
        if ($client->has('client_secret')) {
375
            $jwk = new JWK([
376
                'kty' => 'oct',
377
                'use' => 'sig',
378
                '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

378
                'k' => Base64Url::encode(/** @scrutinizer ignore-type */ $client->get('client_secret')),
Loading history...
379
            ]);
380
            $keyset = $keyset->with($jwk);
381
        }
382
        if ($client->has('jwks_uri') && null !== $this->jkuFactory) {
383
            $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

383
            $jwks_uri = $this->jkuFactory->loadFromUrl(/** @scrutinizer ignore-type */ $client->get('jwks_uri'));
Loading history...
384
            foreach ($jwks_uri as $jwk) {
385
                $keyset = $keyset->with($jwk);
386
            }
387
        }
388
        if (\in_array('none', $this->getSupportedSignatureAlgorithms(), true)) {
389
            $keyset = $keyset->with(new JWK([
390
                'kty' => 'none',
391
                'alg' => 'none',
392
                'use' => 'sig',
393
            ]));
394
        }
395
396
        Assertion::greaterThan($keyset->count(), 0, 'The client has no key or key set.');
397
398
        return $keyset;
399
    }
400
}
401