Failed Conditions
Push — master ( 819484...23fc45 )
by Florent
03:33
created

AuthorizationRequestLoader   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 335
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 139
dl 0
loc 335
rs 5.04
c 0
b 0
f 0
wmc 57

24 Methods

Rating   Name   Duplication   Size   Complexity  
A checkUsedAlgorithm() 0 4 1
A isRequestObjectReferenceSupportEnabled() 0 3 1
A checkRequestUri() 0 18 6
A checkIssuerAndClientId() 0 4 3
A getSupportedContentEncryptionAlgorithms() 0 3 2
A isRequestObjectSupportEnabled() 0 3 1
A downloadContent() 0 7 1
A tryToLoadEncryptedRequest() 0 19 5
A isEncryptedRequestSupportEnabled() 0 3 1
A checkAlgorithms() 0 9 2
A enableJkuSupport() 0 3 1
A load() 0 13 3
A __construct() 0 3 1
A enableEncryptedRequestObjectSupport() 0 7 1
A getClient() 0 8 4
A createFromRequestUriParameter() 0 15 3
B getClientKeySet() 0 34 8
A enableRequestObjectReferenceSupport() 0 6 1
A loadRequestObject() 0 30 3
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 Assert\Assertion;
17
use Base64Url\Base64Url;
18
use Http\Client\HttpClient;
19
use Jose\Component\Checker\ClaimCheckerManager;
20
use Jose\Component\Core\Converter\StandardConverter;
21
use Jose\Component\Core\JWK;
22
use Jose\Component\Core\JWKSet;
23
use Jose\Component\Encryption\JWELoader;
24
use Jose\Component\KeyManagement\JKUFactory;
25
use Jose\Component\Signature\JWS;
26
use Jose\Component\Signature\JWSVerifier;
27
use Jose\Component\Signature\Serializer\CompactSerializer;
28
use OAuth2Framework\Component\Core\Client\Client;
29
use OAuth2Framework\Component\Core\Client\ClientId;
30
use OAuth2Framework\Component\Core\Client\ClientRepository;
31
use OAuth2Framework\Component\Core\Message\OAuth2Error;
32
use Psr\Http\Message\ServerRequestInterface;
33
use Zend\Diactoros\Request;
34
35
class AuthorizationRequestLoader
36
{
37
    /**
38
     * @var ClientRepository
39
     */
40
    private $clientRepository;
41
42
    /**
43
     * @var bool
44
     */
45
    private $requestObjectAllowed = false;
46
47
    /**
48
     * @var bool
49
     */
50
    private $requestObjectReferenceAllowed = false;
51
52
    /**
53
     * @var JWKSet
54
     */
55
    private $keyEncryptionKeySet;
56
57
    /**
58
     * @var bool
59
     */
60
    private $requireRequestUriRegistration = true;
61
62
    /**
63
     * @var bool
64
     */
65
    private $requireEncryption = false;
66
67
    /**
68
     * @var HttpClient|null
69
     */
70
    private $client;
71
72
    /**
73
     * @var JWSVerifier
74
     */
75
    private $jwsVerifier;
76
77
    /**
78
     * @var ClaimCheckerManager
79
     */
80
    private $claimCheckerManager;
81
82
    /**
83
     * @var JWELoader
84
     */
85
    private $jweLoader;
86
87
    /**
88
     * @var JKUFactory|null
89
     */
90
    private $jkuFactory;
91
92
    public function __construct(ClientRepository $clientRepository)
93
    {
94
        $this->clientRepository = $clientRepository;
95
    }
96
97
    public function isRequestUriRegistrationRequired(): bool
98
    {
99
        return $this->requireRequestUriRegistration;
100
    }
101
102
    public function isRequestObjectSupportEnabled(): bool
103
    {
104
        return $this->requestObjectAllowed;
105
    }
106
107
    public function isRequestObjectReferenceSupportEnabled(): bool
108
    {
109
        return $this->requestObjectReferenceAllowed;
110
    }
111
112
    /**
113
     * @return string[]
114
     */
115
    public function getSupportedSignatureAlgorithms(): array
116
    {
117
        return null === $this->jwsVerifier ? [] : $this->jwsVerifier->getSignatureAlgorithmManager()->list();
118
    }
119
120
    /**
121
     * @return string[]
122
     */
123
    public function getSupportedKeyEncryptionAlgorithms(): array
124
    {
125
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
126
    }
127
128
    /**
129
     * @return string[]
130
     */
131
    public function getSupportedContentEncryptionAlgorithms(): array
132
    {
133
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
134
    }
135
136
    public function enableSignedRequestObjectSupport(JWSVerifier $jwsVerifier, ClaimCheckerManager $claimCheckerManager)
137
    {
138
        $this->jwsVerifier = $jwsVerifier;
139
        $this->claimCheckerManager = $claimCheckerManager;
140
        $this->requestObjectAllowed = true;
141
    }
142
143
    public function enableRequestObjectReferenceSupport(HttpClient $client, bool $requireRequestUriRegistration): void
144
    {
145
        Assertion::true($this->isRequestObjectSupportEnabled(), 'Request object support must be enabled first.');
146
        $this->requestObjectReferenceAllowed = true;
147
        $this->requireRequestUriRegistration = $requireRequestUriRegistration;
148
        $this->client = $client;
149
    }
150
151
    public function enableEncryptedRequestObjectSupport(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $requireEncryption): void
152
    {
153
        Assertion::true($this->isRequestObjectSupportEnabled(), 'Request object support must be enabled first.');
154
        Assertion::greaterThan($keyEncryptionKeySet->count(), 0, 'The encryption key set must have at least one key.');
155
        $this->jweLoader = $jweLoader;
156
        $this->requireEncryption = $requireEncryption;
157
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
158
    }
159
160
    public function enableJkuSupport(JKUFactory $jkuFactory): void
161
    {
162
        $this->jkuFactory = $jkuFactory;
163
    }
164
165
    public function isEncryptedRequestSupportEnabled(): bool
166
    {
167
        return null !== $this->keyEncryptionKeySet;
168
    }
169
170
    public function load(ServerRequestInterface $request): AuthorizationRequest
171
    {
172
        $client = null;
173
        $params = $request->getQueryParams();
174
        if (\array_key_exists('request', $params)) {
175
            $params = $this->createFromRequestParameter($params, $client);
176
        } elseif (\array_key_exists('request_uri', $params)) {
177
            $params = $this->createFromRequestUriParameter($params, $client);
178
        } else {
179
            $client = $this->getClient($params);
180
        }
181
182
        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

182
        return new AuthorizationRequest(/** @scrutinizer ignore-type */ $client, $params);
Loading history...
183
    }
184
185
    private function createFromRequestParameter(array $params, Client &$client = null): array
186
    {
187
        if (false === $this->isRequestObjectSupportEnabled()) {
188
            throw OAuth2Error::requestNotSupported('The parameter "request" is not supported.');
189
        }
190
        $request = $params['request'];
191
        if (!\is_string($request)) {
192
            throw OAuth2Error::requestNotSupported('The parameter "request" must be an assertion.');
193
        }
194
195
        $params = $this->loadRequestObject($params, $request, $client);
196
        $this->checkIssuerAndClientId($params);
197
198
        return $params;
199
    }
200
201
    private function createFromRequestUriParameter(array $params, Client &$client = null): array
202
    {
203
        if (false === $this->isRequestObjectReferenceSupportEnabled()) {
204
            throw OAuth2Error::requestUriNotSupported('The parameter "request_uri" is not supported.');
205
        }
206
        $requestUri = $params['request_uri'];
207
        if (\Safe\preg_match('#/\.\.?(/|$)#', $requestUri)) {
208
            throw OAuth2Error::invalidRequestUri('The request Uri is not allowed.');
209
        }
210
        $content = $this->downloadContent($requestUri);
211
        $params = $this->loadRequestObject($params, $content, $client);
212
        $this->checkRequestUri($client, $requestUri);
213
        $this->checkIssuerAndClientId($params);
214
215
        return $params;
216
    }
217
218
    private function checkIssuerAndClientId(array $params): void
219
    {
220
        if (\array_key_exists('iss', $params) && \array_key_exists('client_id', $params)) {
221
            Assertion::eq($params['iss'], $params['client_id'], 'The issuer of the request object is not the client who requests the authorization.');
222
        }
223
    }
224
225
    private function checkRequestUri(Client $client, $requestUri)
226
    {
227
        $storedRequestUris = $client->has('request_uris') ? $client->get('request_uris') : [];
228
        if (empty($storedRequestUris)) {
229
            if ($this->isRequestUriRegistrationRequired()) {
230
                throw OAuth2Error::invalidRequestUri('The clients shall register at least one request object uri.');
231
            }
232
233
            return;
234
        }
235
236
        foreach ($storedRequestUris as $storedRequestUri) {
237
            if (0 === \strcasecmp(\mb_substr($requestUri, 0, \mb_strlen($storedRequestUri, '8bit'), '8bit'), $storedRequestUri)) {
238
                return;
239
            }
240
        }
241
242
        throw OAuth2Error::invalidRequestUri('The request Uri is not allowed.');
243
    }
244
245
    private function loadRequestObject(array $params, string $request, Client &$client = null): array
246
    {
247
        // FIXME Can be
248
        // - encrypted (not supported)
249
        // - encrypted and signed (supported)
250
        // - signed (supported)
251
        $request = $this->tryToLoadEncryptedRequest($request);
252
253
        try {
254
            $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

254
            $jsonConverter = /** @scrutinizer ignore-deprecated */ new StandardConverter();
Loading history...
255
            $serializer = new CompactSerializer($jsonConverter);
256
            $jwt = $serializer->unserialize($request);
257
258
            $claims = $jsonConverter->decode(
259
                $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

259
                /** @scrutinizer ignore-type */ $jwt->getPayload()
Loading history...
260
            );
261
            Assertion::isArray($claims, 'Invalid assertion. The payload must contain claims.');
262
            $this->claimCheckerManager->check($claims);
263
            $parameters = \array_merge($params, $claims);
264
            $client = $this->getClient($parameters);
265
266
            $public_key_set = $this->getClientKeySet($client);
267
            $this->checkAlgorithms($jwt, $client);
268
            Assertion::true($this->jwsVerifier->verifyWithKeySet($jwt, $public_key_set, 0), 'The verification of the request object failed.'); //FIXME: header checker should be used
269
270
            return $parameters;
271
        } catch (OAuth2Error $e) {
272
            throw $e;
273
        } catch (\Throwable $e) {
274
            throw OAuth2Error::invalidRequestObject($e->getMessage(), [], $e);
275
        }
276
    }
277
278
    private function tryToLoadEncryptedRequest(string $request): string
279
    {
280
        if (null === $this->jweLoader) {
281
            return $request;
282
        }
283
284
        try {
285
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($request, $this->keyEncryptionKeySet, $recipient);
286
            Assertion::eq(1, $jwe->countRecipients(), 'The request must use the compact serialization mode.');
287
288
            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...
289
        } catch (OAuth2Error $e) {
290
            throw $e;
291
        } catch (\Throwable $e) {
292
            if (true === $this->requireEncryption) {
293
                throw OAuth2Error::invalidRequestObject($e->getMessage(), [], $e);
294
            }
295
296
            return $request;
297
        }
298
    }
299
300
    private function checkAlgorithms(JWS $jws, Client $client)
301
    {
302
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
303
        Assertion::string($signatureAlgorithm, 'Invalid algorithm parameter in Request Object.');
304
        if ($client->has('request_object_signing_alg')) {
305
            Assertion::eq($signatureAlgorithm, $client->get('request_object_signing_alg'), 'Request Object signature algorithm not allowed for the client.');
306
        }
307
308
        $this->checkUsedAlgorithm($signatureAlgorithm);
309
    }
310
311
    private function checkUsedAlgorithm(string $algorithm): void
312
    {
313
        $supportedAlgorithms = $this->getSupportedSignatureAlgorithms();
314
        Assertion::inArray($algorithm, $supportedAlgorithms, \Safe\sprintf('The algorithm "%s" is not allowed for request object signatures. Please use one of the following algorithm(s): %s', $algorithm, \implode(', ', $supportedAlgorithms)));
315
    }
316
317
    private function downloadContent(string $url): string
318
    {
319
        $request = new Request($url, 'GET');
320
        $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

320
        /** @scrutinizer ignore-call */ 
321
        $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...
321
        Assertion::eq(200, $response->getStatusCode(), 'Unable to load the request object');
322
323
        return $response->getBody()->getContents();
324
    }
325
326
    private function getClient(array $params): Client
327
    {
328
        $client = \array_key_exists('client_id', $params) ? $this->clientRepository->find(new ClientId($params['client_id'])) : null;
329
        if (!$client instanceof Client || true === $client->isDeleted()) {
330
            throw OAuth2Error::invalidRequest('Parameter "client_id" missing or invalid.');
331
        }
332
333
        return $client;
334
    }
335
336
    private function getClientKeySet(Client $client): JWKSet
337
    {
338
        $keyset = JWKSet::createFromKeys([]);
339
        if ($client->has('jwks')) {
340
            $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

340
            $jwks = JWKSet::createFromJson(/** @scrutinizer ignore-type */ $client->get('jwks'));
Loading history...
341
            foreach ($jwks as $jwk) {
342
                $keyset = $keyset->with($jwk);
343
            }
344
        }
345
        if ($client->has('client_secret')) {
346
            $jwk = JWK::create([
347
                'kty' => 'oct',
348
                'use' => 'sig',
349
                '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

349
                'k' => Base64Url::encode(/** @scrutinizer ignore-type */ $client->get('client_secret')),
Loading history...
350
            ]);
351
            $keyset = $keyset->with($jwk);
352
        }
353
        if ($client->has('jwks_uri') && null !== $this->jkuFactory) {
354
            $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

354
            $jwks_uri = $this->jkuFactory->loadFromUrl(/** @scrutinizer ignore-type */ $client->get('jwks_uri'));
Loading history...
355
            foreach ($jwks_uri as $jwk) {
356
                $keyset = $keyset->with($jwk);
357
            }
358
        }
359
        if (\in_array('none', $this->getSupportedSignatureAlgorithms(), true)) {
360
            $keyset = $keyset->with(JWK::create([
361
                'kty' => 'none',
362
                'alg' => 'none',
363
                'use' => 'sig',
364
            ]));
365
        }
366
367
        Assertion::greaterThan($keyset->count(), 0, 'The client has no key or key set.');
368
369
        return $keyset;
370
    }
371
}
372