Failed Conditions
Push — master ( b0e939...893034 )
by Florent
19:07
created

AuthorizationRequestLoader::loadRequestObject()   B

Complexity

Conditions 5
Paths 27

Size

Total Lines 36
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 8.439
c 0
b 0
f 0
cc 5
eloc 22
nc 27
nop 3
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
     * @param ClientRepository $clientRepository
95
     */
96
    public function __construct(ClientRepository $clientRepository)
97
    {
98
        $this->clientRepository = $clientRepository;
99
    }
100
101
    /**
102
     * @return bool
103
     */
104
    public function isRequestUriRegistrationRequired(): bool
105
    {
106
        return $this->requireRequestUriRegistration;
107
    }
108
109
    /**
110
     * @return bool
111
     */
112
    public function isRequestObjectSupportEnabled(): bool
113
    {
114
        return $this->requestObjectAllowed;
115
    }
116
117
    /**
118
     * @return bool
119
     */
120
    public function isRequestObjectReferenceSupportEnabled(): bool
121
    {
122
        return $this->requestObjectReferenceAllowed;
123
    }
124
125
    /**
126
     * @return string[]
127
     */
128
    public function getSupportedSignatureAlgorithms(): array
129
    {
130
        return null === $this->jwsVerifier ? [] : $this->jwsVerifier->getSignatureAlgorithmManager()->list();
131
    }
132
133
    /**
134
     * @return string[]
135
     */
136
    public function getSupportedKeyEncryptionAlgorithms(): array
137
    {
138
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
139
    }
140
141
    /**
142
     * @return string[]
143
     */
144
    public function getSupportedContentEncryptionAlgorithms(): array
145
    {
146
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
147
    }
148
149
    /**
150
     * @param JWSVerifier         $jwsVerifier
151
     * @param ClaimCheckerManager $claimCheckerManager
152
     */
153
    public function enableSignedRequestObjectSupport(JWSVerifier $jwsVerifier, ClaimCheckerManager $claimCheckerManager)
154
    {
155
        $this->jwsVerifier = $jwsVerifier;
156
        $this->claimCheckerManager = $claimCheckerManager;
157
        $this->requestObjectAllowed = true;
158
    }
159
160
    /**
161
     * @param HttpClient $client
162
     * @param bool       $requireRequestUriRegistration
163
     */
164
    public function enableRequestObjectReferenceSupport(HttpClient $client, bool $requireRequestUriRegistration)
165
    {
166
        if (!$this->isRequestObjectSupportEnabled()) {
167
            throw new \InvalidArgumentException('Request object support must be enabled first.');
168
        }
169
        $this->requestObjectReferenceAllowed = true;
170
        $this->requireRequestUriRegistration = $requireRequestUriRegistration;
171
        $this->client = $client;
172
    }
173
174
    /**
175
     * @param JWELoader $jweLoader
176
     * @param JWKSet    $keyEncryptionKeySet
177
     * @param bool      $requireEncryption
178
     *
179
     * @throws \InvalidArgumentException
180
     */
181
    public function enableEncryptedRequestObjectSupport(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $requireEncryption)
182
    {
183
        if (!$this->isRequestObjectSupportEnabled()) {
184
            throw new \InvalidArgumentException('Request object support must be enabled first.');
185
        }
186
        if (0 === $keyEncryptionKeySet->count()) {
187
            throw new \InvalidArgumentException('The encryption key set must have at least one key.');
188
        }
189
        $this->jweLoader = $jweLoader;
190
        $this->requireEncryption = $requireEncryption;
191
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
192
    }
193
194
    /**
195
     * @param JKUFactory $jkuFactory
196
     */
197
    public function enableJkuSupport(JKUFactory $jkuFactory)
198
    {
199
        $this->jkuFactory = $jkuFactory;
200
    }
201
202
    /**
203
     * @return bool
204
     */
205
    public function isEncryptedRequestSupportEnabled(): bool
206
    {
207
        return null !== $this->keyEncryptionKeySet;
208
    }
209
210
    /**
211
     * @param ServerRequestInterface $request
212
     *
213
     * @return Authorization
214
     *
215
     * @throws OAuth2Message
216
     * @throws \Exception
217
     * @throws \Http\Client\Exception
218
     */
219
    public function load(ServerRequestInterface $request): Authorization
220
    {
221
        $client = null;
222
        $params = $request->getQueryParams();
223
        if (array_key_exists('request', $params)) {
224
            $params = $this->createFromRequestParameter($params, $client);
225
        } elseif (array_key_exists('request_uri', $params)) {
226
            $params = $this->createFromRequestUriParameter($params, $client);
227
        } else {
228
            $client = $this->getClient($params);
229
        }
230
231
        return Authorization::create($client, $params);
0 ignored issues
show
Bug introduced by
It seems like $client defined by null on line 221 can be null; however, OAuth2Framework\Componen...Authorization::create() 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...
232
    }
233
234
    /**
235
     * @param array  $params
236
     * @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...
237
     *
238
     * @throws OAuth2Message
239
     *
240
     * @return array
241
     */
242
    private function createFromRequestParameter(array $params, Client &$client = null): array
243
    {
244
        if (false === $this->isRequestObjectSupportEnabled()) {
245
            throw new OAuth2Message(400, OAuth2Message::ERROR_REQUEST_NOT_SUPPORTED, 'The parameter "request" is not supported.');
246
        }
247
        $request = $params['request'];
248
        if (!is_string($request)) {
249
            throw new OAuth2Message(400, OAuth2Message::ERROR_REQUEST_NOT_SUPPORTED, 'The parameter "request" must be an assertion.');
250
        }
251
252
        $params = $this->loadRequestObject($params, $request, $client);
253
        $this->checkIssuerAndClientId($params);
254
255
        return $params;
256
    }
257
258
    /**
259
     * @param array  $params
260
     * @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...
261
     *
262
     * @return array
263
     *
264
     * @throws OAuth2Message
265
     * @throws \Exception
266
     * @throws \Http\Client\Exception
267
     */
268
    private function createFromRequestUriParameter(array $params, Client &$client = null): array
269
    {
270
        if (false === $this->isRequestObjectReferenceSupportEnabled()) {
271
            throw new OAuth2Message(400, OAuth2Message::ERROR_REQUEST_URI_NOT_SUPPORTED, 'The parameter "request_uri" is not supported.');
272
        }
273
        $requestUri = $params['request_uri'];
274
        if (preg_match('#/\.\.?(/|$)#', $requestUri)) {
275
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_URI, 'The request Uri is not allowed.');
276
        }
277
        $content = $this->downloadContent($requestUri);
278
        $params = $this->loadRequestObject($params, $content, $client);
279
        $this->checkRequestUri($client, $requestUri);
0 ignored issues
show
Bug introduced by
It seems like $client defined by parameter $client on line 268 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...
280
        $this->checkIssuerAndClientId($params);
281
282
        return $params;
283
    }
284
285
    /**
286
     * @param array $params
287
     *
288
     * @throws \InvalidArgumentException
289
     */
290
    private function checkIssuerAndClientId(array $params)
291
    {
292
        if (array_key_exists('iss', $params) && array_key_exists('client_id', $params)) {
293
            if ($params['iss'] !== $params['client_id']) {
294
                throw new \InvalidArgumentException('The issuer of the request object is not the client who requests the authorization.');
295
            }
296
        }
297
    }
298
299
    /**
300
     * @param Client $client
301
     * @param string $requestUri
302
     *
303
     * @throws OAuth2Message
304
     */
305
    private function checkRequestUri(Client $client, $requestUri)
306
    {
307
        $storedRequestUris = $client->has('request_uris') ? $client->get('request_uris') : [];
308
        if (empty($storedRequestUris)) {
309
            if ($this->isRequestUriRegistrationRequired()) {
310
                throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_URI, 'The clients shall register at least one request object uri.');
311
            }
312
313
            return;
314
        }
315
316
        foreach ($storedRequestUris as $storedRequestUri) {
317
            if (0 === strcasecmp(mb_substr($requestUri, 0, mb_strlen($storedRequestUri, '8bit'), '8bit'), $storedRequestUri)) {
318
                return;
319
            }
320
        }
321
322
        throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_URI, 'The request Uri is not allowed.');
323
    }
324
325
    /**
326
     * @param array       $params
327
     * @param string      $request
328
     * @param Client|null $client
329
     *
330
     * @throws OAuth2Message
331
     *
332
     * @return array
333
     */
334
    private function loadRequestObject(array $params, string $request, Client &$client = null): array
335
    {
336
        // FIXME Can be
337
        // - encrypted (not supported)
338
        // - encrypted and signed (supported)
339
        // - signed (supported)
340
        $request = $this->tryToLoadEncryptedRequest($request);
341
342
        try {
343
            $jsonConverter = new StandardConverter();
344
            $serializer = new CompactSerializer($jsonConverter);
345
            $jwt = $serializer->unserialize($request);
346
347
            $claims = $jsonConverter->decode(
348
                $jwt->getPayload()
349
            );
350
            if (!is_array($claims)) {
351
                throw new \InvalidArgumentException('Invalid assertion. The payload must contain claims.');
352
            }
353
            $this->claimCheckerManager->check($claims);
354
            $parameters = array_merge($params, $claims);
355
            $client = $this->getClient($parameters);
356
357
            $public_key_set = $this->getClientKeySet($client);
358
            $this->checkAlgorithms($jwt, $client);
359
            if (!$this->jwsVerifier->verifyWithKeySet($jwt, $public_key_set, 0)) { //FIXME: header checker should be used
360
                throw new \InvalidArgumentException('The verification of the request object failed.');
361
            }
362
363
            return $parameters;
364
        } catch (OAuth2Message $e) {
365
            throw $e;
366
        } catch (\Exception $e) {
367
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), [], $e);
368
        }
369
    }
370
371
    /**
372
     * @param string $request
373
     *
374
     * @return string
375
     *
376
     * @throws OAuth2Message
377
     */
378
    private function tryToLoadEncryptedRequest(string $request): string
379
    {
380
        if (null === $this->jweLoader) {
381
            return $request;
382
        }
383
384
        try {
385
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($request, $this->keyEncryptionKeySet, $recipient);
386
            if (1 !== $jwe->countRecipients()) {
387
                throw new \InvalidArgumentException('The request must use the compact serialization mode.');
388
            }
389
390
            return $jwe->getPayload();
391
        } catch (\Exception $e) {
392
            if (true === $this->requireEncryption) {
393
                throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), [], $e);
394
            }
395
396
            return $request;
397
        }
398
    }
399
400
    /**
401
     * @param JWS    $jws
402
     * @param Client $client
403
     *
404
     * @throws \InvalidArgumentException
405
     */
406
    private function checkAlgorithms(JWS $jws, Client $client)
407
    {
408
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
409
        if ($client->has('request_object_signing_alg') && $signatureAlgorithm !== $client->get('request_object_signing_alg')) {
410
            throw new \InvalidArgumentException('Request Object signature algorithm not allowed for the client.');
411
        }
412
413
        $this->checkUsedAlgorithm($signatureAlgorithm);
414
    }
415
416
    private function checkUsedAlgorithm(string $algorithm): void
417
    {
418
        $supportedAlgorithms = $this->getSupportedSignatureAlgorithms();
419
        if (!in_array($algorithm, $supportedAlgorithms)) {
420
            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)));
421
        }
422
    }
423
424
    /**
425
     * @param string $url
426
     *
427
     * @return string
428
     *
429
     * @throws OAuth2Message
430
     * @throws \Exception
431
     * @throws \Http\Client\Exception
432
     */
433
    private function downloadContent(string $url): string
434
    {
435
        $request = new Request($url, 'GET');
436
        $response = $this->client->sendRequest($request);
437
        if (200 !== $response->getStatusCode()) {
438
            throw new \InvalidArgumentException();
439
        }
440
441
        $content = $response->getBody()->getContents();
442
        if (!is_string($content)) {
443
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_URI, 'Unable to get content.');
444
        }
445
446
        return $content;
447
    }
448
449
    /**
450
     * @param array $params
451
     *
452
     * @throws OAuth2Message
453
     *
454
     * @return Client
455
     */
456
    private function getClient(array $params): Client
457
    {
458
        $client = array_key_exists('client_id', $params) ? $this->clientRepository->find(ClientId::create($params['client_id'])) : null;
459
        if (!$client instanceof Client || true === $client->isDeleted()) {
460
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, 'Parameter "client_id" missing or invalid.');
461
        }
462
463
        return $client;
464
    }
465
466
    /**
467
     * @param Client $client
468
     *
469
     * @return JWKSet
470
     */
471
    private function getClientKeySet(Client $client): JWKSet
472
    {
473
        $keyset = JWKSet::createFromKeys([]);
474
        if ($client->has('jwks')) {
475
            $jwks = JWKSet::createFromJson($client->get('jwks'));
476
            foreach ($jwks as $jwk) {
477
                $keyset = $keyset->with($jwk);
478
            }
479
        }
480
        if ($client->has('client_secret')) {
481
            $jwk = JWK::create([
482
                'kty' => 'oct',
483
                'use' => 'sig',
484
                'k' => Base64Url::encode($client->get('client_secret')),
485
            ]);
486
            $keyset = $keyset->with($jwk);
487
        }
488
        if ($client->has('jwks_uri') && null !== $this->jkuFactory) {
489
            $jwks_uri = $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
490
            foreach ($jwks_uri as $jwk) {
491
                $keyset = $keyset->with($jwk);
492
            }
493
        }
494
        if (in_array('none', $this->getSupportedSignatureAlgorithms())) {
495
            $keyset = $keyset->with(JWK::create([
496
                'kty' => 'none',
497
                'alg' => 'none',
498
                'use' => 'sig',
499
            ]));
500
        }
501
502
        if ($keyset->count() === 0) {
503
            throw new \InvalidArgumentException('The client has no key or key set.');
504
        }
505
506
        return $keyset;
507
    }
508
}
509