Failed Conditions
Push — master ( 94c29b...82cb82 )
by Florent
46:48 queued 42:07
created

createFromRequestUriParameter()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 11
nc 3
nop 2
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
        } catch (OAuth2Message $e) {
363
            throw $e;
364
        } catch (\Exception $e) {
365
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), [], $e);
366
        }
367
368
        return $parameters;
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);
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...
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
        if ($client->has('request_object_signing_alg') && $jws->getSignature(0)->getProtectedHeaderParameter('alg') !== $client->get('request_object_signing_alg')) {
409
            throw new \InvalidArgumentException('Request Object signature algorithm not allowed for the client.');
410
        }
411
412
        $this->checkUsedAlgorithm($jws->getSignature(0)->getProtectedHeaderParameter('alg'));
413
    }
414
415
    /**
416
     * @param string $algorithm
417
     */
418
    private function checkUsedAlgorithm(string $algorithm)
419
    {
420
        $supportedAlgorithms = $this->getSupportedSignatureAlgorithms();
421
        if (!in_array($algorithm, $supportedAlgorithms)) {
422
            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)));
423
        }
424
    }
425
426
    /**
427
     * @param string $url
428
     *
429
     * @return string
430
     *
431
     * @throws OAuth2Message
432
     * @throws \Exception
433
     * @throws \Http\Client\Exception
434
     */
435
    private function downloadContent(string $url): string
436
    {
437
        $request = new Request($url, 'GET');
438
        $response = $this->client->sendRequest($request);
439
        if (200 !== $response->getStatusCode()) {
440
            throw new \InvalidArgumentException();
441
        }
442
443
        $content = $response->getBody()->getContents();
444
        if (!is_string($content)) {
445
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST_URI, 'Unable to get content.');
446
        }
447
448
        return $content;
449
    }
450
451
    /**
452
     * @param array $params
453
     *
454
     * @throws OAuth2Message
455
     *
456
     * @return Client
457
     */
458
    private function getClient(array $params): Client
459
    {
460
        $client = array_key_exists('client_id', $params) ? $this->clientRepository->find(ClientId::create($params['client_id'])) : null;
461
        if (!$client instanceof Client || true === $client->isDeleted()) {
462
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, 'Parameter "client_id" missing or invalid.');
463
        }
464
465
        return $client;
466
    }
467
468
    /**
469
     * @param Client $client
470
     *
471
     * @return JWKSet
472
     */
473
    private function getClientKeySet(Client $client): JWKSet
474
    {
475
        switch (true) {
476
            case $client->has('jwks') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod():
477
                return JWKSet::createFromJson($client->get('jwks'));
478
            case $client->has('client_secret') && 'client_secret_jwt' === $client->getTokenEndpointAuthenticationMethod():
479
                $jwk = JWK::create([
480
                    'kty' => 'oct',
481
                    'use' => 'sig',
482
                    'k' => Base64Url::encode($client->get('client_secret')),
483
                ]);
484
485
                return JWKSet::createFromKeys([$jwk]);
486
            case $client->has('jwks_uri') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod() && null !== $this->jkuFactory:
487
                return $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
488
            default:
489
                throw new \InvalidArgumentException('The client has no key or key set.');
490
        }
491
    }
492
}
493