Failed Conditions
Push — ng ( ede6c5...efffe8 )
by Florent
11:50
created

createFromRequestParameter()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 11
nc 3
nop 1
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\Server\AuthorizationEndpoint;
15
16
use Http\Client\HttpClient;
17
use Jose\Component\Checker\ClaimCheckerManager;
18
use Jose\Component\Core\JWKSet;
19
use Jose\Component\Encryption\JWELoader;
20
use Jose\Component\Signature\JWS;
21
use Jose\Component\Signature\JWSLoader;
22
use OAuth2Framework\Component\Server\Core\Client\Client;
23
use OAuth2Framework\Component\Server\Core\Client\ClientId;
24
use OAuth2Framework\Component\Server\Core\Client\ClientRepository;
25
use OAuth2Framework\Component\Server\Core\Exception\OAuth2Exception;
26
use Psr\Http\Message\ServerRequestInterface;
27
use Zend\Diactoros\Request;
28
29
final class AuthorizationRequestLoader
30
{
31
    /**
32
     * @var ClientRepository
33
     */
34
    private $clientRepository;
35
36
    /**
37
     * @var bool
38
     */
39
    private $requestObjectAllowed = false;
40
41
    /**
42
     * @var bool
43
     */
44
    private $requestObjectReferenceAllowed = false;
45
46
    /**
47
     * @var JWKSet
48
     */
49
    private $keyEncryptionKeySet = null;
50
51
    /**
52
     * @var bool
53
     */
54
    private $requireRequestUriRegistration = true;
55
56
    /**
57
     * @var bool
58
     */
59
    private $requireEncryption = false;
60
61
    /**
62
     * @var string[]
63
     */
64
    private $mandatoryClaims = [];
65
66
    /**
67
     * @var null|HttpClient
68
     */
69
    private $client = null;
70
71
    /**
72
     * @var JWSLoader
73
     */
74
    private $jwsLoader = null;
75
76
    /**
77
     * @var ClaimCheckerManager
78
     */
79
    private $claimCheckerManager = null;
80
81
    /**
82
     * @var JWELoader
83
     */
84
    private $jweLoader = null;
85
86
    /**
87
     * AuthorizationRequestLoader constructor.
88
     *
89
     * @param ClientRepository $clientRepository
90
     */
91
    public function __construct(ClientRepository $clientRepository)
92
    {
93
        $this->clientRepository = $clientRepository;
94
    }
95
96
    /**
97
     * @return bool
98
     */
99
    public function isRequestUriRegistrationRequired(): bool
100
    {
101
        return $this->requireRequestUriRegistration;
102
    }
103
104
    /**
105
     * @return bool
106
     */
107
    public function isRequestObjectSupportEnabled(): bool
108
    {
109
        return $this->requestObjectAllowed;
110
    }
111
112
    /**
113
     * @return bool
114
     */
115
    public function isRequestObjectReferenceSupportEnabled(): bool
116
    {
117
        return $this->requestObjectReferenceAllowed;
118
    }
119
120
    /**
121
     * @return string[]
122
     */
123
    public function getSupportedSignatureAlgorithms(): array
124
    {
125
        return null === $this->jwsLoader ? [] : $this->jwsLoader->getSignatureAlgorithmManager()->list();
0 ignored issues
show
Bug introduced by
The method getSignatureAlgorithmManager() does not seem to exist on object<Jose\Component\Signature\JWSLoader>.

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...
126
    }
127
128
    /**
129
     * @return string[]
130
     */
131
    public function getSupportedKeyEncryptionAlgorithms(): array
132
    {
133
        return null === $this->jweLoader ? [] : $this->jweLoader->getKeyEncryptionAlgorithmManager()->list();
134
    }
135
136
    /**
137
     * @return string[]
138
     */
139
    public function getSupportedContentEncryptionAlgorithms(): array
140
    {
141
        return null === $this->jweLoader ? [] : $this->jweLoader->getContentEncryptionAlgorithmManager()->list();
142
    }
143
144
    /**
145
     * @param JWSLoader           $jwsLoader
146
     * @param ClaimCheckerManager $claimCheckerManager
147
     * @param string[]            $mandatoryClaims
148
     */
149
    public function enableRequestObjectSupport(JWSLoader $jwsLoader, ClaimCheckerManager $claimCheckerManager, array $mandatoryClaims = [])
150
    {
151
        foreach ($mandatoryClaims as $mandatoryClaim) {
152
            if (!is_string($mandatoryClaim)) {
153
                throw new \InvalidArgumentException('The mandatory claims array should contain only claims.');
154
            }
155
        }
156
        $this->jwsLoader = $jwsLoader;
157
        $this->claimCheckerManager = $claimCheckerManager;
158
        $this->requestObjectAllowed = true;
159
        $this->mandatoryClaims = $mandatoryClaims;
160
    }
161
162
    /**
163
     * @param HttpClient $client
164
     * @param bool       $requireRequestUriRegistration
165
     */
166
    public function enableRequestObjectReferenceSupport(HttpClient $client, bool $requireRequestUriRegistration)
167
    {
168
        if (!$this->isRequestObjectSupportEnabled()) {
169
            throw new \InvalidArgumentException('Request object support must be enabled first.');
170
        }
171
        $this->requestObjectReferenceAllowed = true;
172
        $this->requireRequestUriRegistration = $requireRequestUriRegistration;
173
        $this->client = $client;
174
    }
175
176
    /**
177
     * @param JWELoader $jweLoader
178
     * @param JWKSet    $keyEncryptionKeySet
179
     * @param bool      $requireEncryption
180
     *
181
     * @throws \InvalidArgumentException
182
     */
183
    public function enableEncryptedRequestObjectSupport(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $requireEncryption)
184
    {
185
        if (!$this->isRequestObjectSupportEnabled()) {
186
            throw new \InvalidArgumentException('Request object support must be enabled first.');
187
        }
188
        if (0 === $keyEncryptionKeySet->count()) {
189
            throw new \InvalidArgumentException('The encryption key set must have at least one key.');
190
        }
191
        $this->jweLoader = $jweLoader;
192
        $this->requireEncryption = $requireEncryption;
193
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
194
    }
195
196
    /**
197
     * @return bool
198
     */
199
    public function isEncryptedRequestsSupportEnabled(): bool
200
    {
201
        return null !== $this->keyEncryptionKeySet;
202
    }
203
204
    /**
205
     * @param ServerRequestInterface $request
206
     *
207
     * @return array
208
     *
209
     * @throws OAuth2Exception
210
     * @throws \Exception
211
     * @throws \Http\Client\Exception
212
     */
213
    public function loadParametersFromRequest(ServerRequestInterface $request): array
214
    {
215
        $params = $request->getQueryParams();
216
        if (array_key_exists('request', $params)) {
217
            $params = $this->createFromRequestParameter($params);
218
        } elseif (array_key_exists('request_uri', $params)) {
219
            $params = $this->createFromRequestUriParameter($params);
220
        } else {
221
            $params = $this->createFromStandardRequest($params);
222
        }
223
224
        $client = $params['client'];
225
        unset($params['client']);
226
227
        return [$client, $params];
228
    }
229
230
    /**
231
     * @param array $params
232
     *
233
     * @throws OAuth2Exception
234
     *
235
     * @return array
236
     */
237
    private function createFromRequestParameter(array $params): array
238
    {
239
        if (false === $this->isRequestObjectSupportEnabled()) {
240
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_REQUEST_NOT_SUPPORTED, 'The parameter "request" is not supported.');
241
        }
242
        $request = $params['request'];
243
        if (!is_string($request)) {
244
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_REQUEST_NOT_SUPPORTED, 'The parameter "request" must be an assertion.');
245
        }
246
247
        $jws = $this->loadRequest($params, $request, $client);
248
        $claims = json_decode($jws->getPayload(), true);
249
        $params = array_merge($params, $claims, ['client' => $client]);
250
        $this->checkIssuerAndClientId($params);
251
252
        return $params;
253
    }
254
255
    /**
256
     * @param array $params
257
     *
258
     * @return array
259
     *
260
     * @throws OAuth2Exception
261
     */
262
    private function createFromStandardRequest(array $params): array
263
    {
264
        $client = $this->getClient($params);
265
266
        return array_merge($params, ['client' => $client]);
267
    }
268
269
    /**
270
     * @param array $params
271
     *
272
     * @return array
273
     *
274
     * @throws OAuth2Exception
275
     * @throws \Exception
276
     * @throws \Http\Client\Exception
277
     */
278
    private function createFromRequestUriParameter(array $params): array
279
    {
280
        if (false === $this->isRequestObjectReferenceSupportEnabled()) {
281
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_REQUEST_URI_NOT_SUPPORTED, 'The parameter "request_uri" is not supported.');
282
        }
283
        $requestUri = $params['request_uri'];
284
285
        $content = $this->downloadContent($requestUri);
286
        $jws = $this->loadRequest($params, $content, $client);
287
        if (true === $this->isRequestUriRegistrationRequired()) {
288
            $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...
289
        }
290
        $claims = json_decode($jws->getPayload(), true);
291
        $params = array_merge($params, $claims, ['client' => $client]);
292
        $this->checkIssuerAndClientId($params);
293
294
        return $params;
295
    }
296
297
    /**
298
     * @param array $params
299
     *
300
     * @throws \InvalidArgumentException
301
     */
302
    private function checkIssuerAndClientId(array $params)
303
    {
304
        if (array_key_exists('iss', $params) && array_key_exists('client_id', $params)) {
305
            if ($params['iss'] !== $params['client_id']) {
306
                throw new \InvalidArgumentException('The issuer of the request object is not the client who requests the authorization.');
307
            }
308
        }
309
    }
310
311
    /**
312
     * @param Client $client
313
     * @param string $requestUri
314
     *
315
     * @throws OAuth2Exception
316
     */
317
    private function checkRequestUri(Client $client, $requestUri)
318
    {
319
        $this->checkRequestUriPathTraversal($requestUri);
320
        $stored_request_uris = $this->getClientRequestUris($client);
321
322
        foreach ($stored_request_uris as $stored_request_uri) {
323
            if (0 === strcasecmp(mb_substr($requestUri, 0, mb_strlen($stored_request_uri, '8bit'), '8bit'), $stored_request_uri)) {
324
                return;
325
            }
326
        }
327
328
        throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_URI, 'The request Uri is not allowed.');
329
    }
330
331
    /**
332
     * @param string $requestUri
333
     *
334
     * @throws OAuth2Exception
335
     */
336
    private function checkRequestUriPathTraversal($requestUri)
337
    {
338
        if (false === Uri::checkUrl($requestUri, false)) {
339
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_CLIENT, 'The request Uri must not contain path traversal.');
340
        }
341
    }
342
343
    /**
344
     * @param Client $client
345
     *
346
     * @throws OAuth2Exception
347
     *
348
     * @return string[]
349
     */
350
    private function getClientRequestUris(Client $client): array
351
    {
352
        if (false === $client->has('request_uris') || empty($requestUris = $client->get('request_uris'))) {
353
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_CLIENT, 'The client must register at least one request Uri.');
354
        }
355
356
        return $requestUris;
357
    }
358
359
    /**
360
     * @param array       $params
361
     * @param string      $request
362
     * @param Client|null $client
363
     *
364
     * @throws OAuth2Exception
365
     *
366
     * @return JWS
367
     */
368
    private function loadRequest(array $params, string $request, Client &$client = null): JWS
369
    {
370
        $request = $this->tryToLoadEncryptedRequest($request);
371
372
        try {
373
            $jwt = $this->jwsLoader->loadAndVerifyWithKeySet($request);
0 ignored issues
show
Bug introduced by
The call to loadAndVerifyWithKeySet() misses some required arguments starting with $keyset.
Loading history...
374
            $this->claimCheckerManager->check($jwt);
0 ignored issues
show
Documentation introduced by
$jwt is of type object<Jose\Component\Signature\JWS>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
375
            $claims = json_decode($jwt->getPayload(), true);
376
377
            $client = $this->getClient(array_merge($params, $claims));
378
            $public_key_set = $client->getPublicKeySet();
379
            Assertion::notNull($public_key_set, 'The client does not have signature capabilities.');
380
            $index = $this->jwsLoader->verifyWithKeySet($jwt, $public_key_set);
0 ignored issues
show
Bug introduced by
The method verifyWithKeySet() does not exist on Jose\Component\Signature\JWSLoader. Did you maybe mean loadAndVerifyWithKeySet()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
381
            $this->checkAlgorithms($jwt, $index, $client);
382
            $missing_claims = array_keys(array_diff_key(array_flip($this->mandatoryClaims), $claims));
383
            Assertion::true(0 === count($missing_claims), 'The following mandatory claims are missing: %s.', implode(', ', $missing_claims));
384
        } catch (\Exception $e) {
385
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), $e);
386
        }
387
388
        return $jwt;
389
    }
390
391
    /**
392
     * @param string $request
393
     *
394
     * @return string
395
     *
396
     * @throws OAuth2Exception
397
     */
398
    private function tryToLoadEncryptedRequest(string $request): string
399
    {
400
        if (null === $this->jweLoader) {
401
            return $request;
402
        }
403
404
        try {
405
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($request, $this->keyEncryptionKeySet, $recipient);
0 ignored issues
show
Bug introduced by
The variable $recipient does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
406
            if (1 !== $jwe->countRecipients()) {
407
                throw new \InvalidArgumentException('The request must use the compact serialization mode.');
408
            }
409
410
            return $jwe->getPayload();
411
        } catch (\Exception $e) {
412
            if (true === $this->requireEncryption) {
413
                throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), $e);
414
            }
415
416
            return $request;
417
        }
418
    }
419
420
    /**
421
     * @param JWS    $jws
422
     * @param int    $index
423
     * @param Client $client
424
     *
425
     * @throws \InvalidArgumentException
426
     */
427
    private function checkAlgorithms(JWS $jws, int $index, Client $client)
428
    {
429
        if (!$client->has('request_object_signing_alg')) {
430
            throw new \InvalidArgumentException('Request Object signature algorithm not defined for the client.');
431
        }
432
        if ($jws->getSignature($index)->getProtectedHeaderParameter('alg') !== $client->get('request_object_signing_alg')) {
433
            throw new \InvalidArgumentException('Request Object signature algorithm not supported by the client.');
434
        }
435
    }
436
437
    /**
438
     * @param $url
439
     *
440
     * @return string
441
     *
442
     * @throws OAuth2Exception
443
     * @throws \Exception
444
     * @throws \Http\Client\Exception
445
     */
446
    private function downloadContent($url): string
447
    {
448
        $request = new Request($url, 'GET');
449
        $response = $this->client->sendRequest($request);
450
        if (200 !== $response->getStatusCode()) {
451
            throw new \InvalidArgumentException();
452
        }
453
454
        $content = $response->getBody()->getContents();
455
        if (!is_string($content)) {
456
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_URI, 'Unable to get content.');
457
        }
458
459
        return $content;
460
    }
461
462
    /**
463
     * @param array $params
464
     *
465
     * @throws OAuth2Exception
466
     *
467
     * @return Client
468
     */
469
    private function getClient(array $params): Client
470
    {
471
        $client = array_key_exists('client_id', $params) ? $this->clientRepository->find(ClientId::create($params['client_id'])) : null;
472
        if (!$client instanceof Client || true === $client->isDeleted()) {
473
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, 'Parameter "client_id" missing or invalid.');
474
        }
475
476
        return $client;
477
    }
478
}
479