Failed Conditions
Push — ng ( 6dd952...0976bc )
by Florent
16:09
created

isEncryptedRequestSupportEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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\Exception\OAuth2Exception;
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 OAuth2Exception
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 OAuth2Exception
239
     *
240
     * @return array
241
     */
242
    private function createFromRequestParameter(array $params, Client &$client = null): array
243
    {
244
        if (false === $this->isRequestObjectSupportEnabled()) {
245
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_REQUEST_NOT_SUPPORTED, 'The parameter "request" is not supported.');
246
        }
247
        $request = $params['request'];
248
        if (!is_string($request)) {
249
            throw new OAuth2Exception(400, OAuth2Exception::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 OAuth2Exception
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 OAuth2Exception(400, OAuth2Exception::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 OAuth2Exception(400, OAuth2Exception::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 OAuth2Exception
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 OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_URI, 'The clients shall register at least one request object uri.');
311
            }
312
            return;
313
        }
314
315
        foreach ($storedRequestUris as $storedRequestUri) {
316
            if (0 === strcasecmp(mb_substr($requestUri, 0, mb_strlen($storedRequestUri, '8bit'), '8bit'), $storedRequestUri)) {
317
                return;
318
            }
319
        }
320
321
        throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_URI, 'The request Uri is not allowed.');
322
    }
323
324
    /**
325
     * @param array       $params
326
     * @param string      $request
327
     * @param Client|null $client
328
     *
329
     * @throws OAuth2Exception
330
     *
331
     * @return array
332
     */
333
    private function loadRequestObject(array $params, string $request, Client &$client = null): array
334
    {
335
        // FIXME Can be
336
        // - encrypted (not supported)
337
        // - encrypted and signed (supported)
338
        // - signed (supported)
339
        $request = $this->tryToLoadEncryptedRequest($request);
340
341
        try {
342
            $jsonConverter = new StandardConverter();
343
            $serializer = new CompactSerializer($jsonConverter);
344
            $jwt = $serializer->unserialize($request);
345
346
            $claims = $jsonConverter->decode(
347
                $jwt->getPayload()
348
            );
349
            if (!is_array($claims)) {
350
                throw new \InvalidArgumentException('Invalid assertion. The payload must contain claims.');
351
            }
352
            $this->claimCheckerManager->check($claims);
353
            $parameters = array_merge($params, $claims);
354
            $client = $this->getClient($parameters);
355
356
            $public_key_set = $this->getClientKeySet($client);
357
            $this->checkAlgorithms($jwt, $client);
358
            if (!$this->jwsVerifier->verifyWithKeySet($jwt, $public_key_set, 0)) { //FIXME: header checker should be used
359
                throw new \InvalidArgumentException('The verification of the request object failed.');
360
            }
361
        } catch (OAuth2Exception $e) {
362
            throw $e;
363
        } catch (\Exception $e) {
364
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), $e);
365
        }
366
367
        return $parameters;
368
    }
369
370
    /**
371
     * @param string $request
372
     *
373
     * @return string
374
     *
375
     * @throws OAuth2Exception
376
     */
377
    private function tryToLoadEncryptedRequest(string $request): string
378
    {
379
        if (null === $this->jweLoader) {
380
            return $request;
381
        }
382
383
        try {
384
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($request, $this->keyEncryptionKeySet, $recipient);
385
            if (1 !== $jwe->countRecipients()) {
386
                throw new \InvalidArgumentException('The request must use the compact serialization mode.');
387
            }
388
389
            return $jwe->getPayload();
390
        } catch (\Exception $e) {
391
            if (true === $this->requireEncryption) {
392
                throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), $e);
393
            }
394
395
            return $request;
396
        }
397
    }
398
399
    /**
400
     * @param JWS    $jws
401
     * @param Client $client
402
     *
403
     * @throws \InvalidArgumentException
404
     */
405
    private function checkAlgorithms(JWS $jws, Client $client)
406
    {
407
        if ($client->has('request_object_signing_alg') && $jws->getSignature(0)->getProtectedHeaderParameter('alg') !== $client->get('request_object_signing_alg')) {
408
            throw new \InvalidArgumentException('Request Object signature algorithm not allowed for the client.');
409
        }
410
411
        $this->checkUsedAlgorithm($jws->getSignature(0)->getProtectedHeaderParameter('alg'));
412
    }
413
414
    /**
415
     * @param string $algorithm
416
     */
417
    private function checkUsedAlgorithm(string $algorithm)
418
    {
419
        $supportedAlgorithms = $this->getSupportedSignatureAlgorithms();
420
        if (!in_array($algorithm, $supportedAlgorithms)) {
421
            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)));
422
        }
423
    }
424
425
    /**
426
     * @param $url
427
     *
428
     * @return string
429
     *
430
     * @throws OAuth2Exception
431
     * @throws \Exception
432
     * @throws \Http\Client\Exception
433
     */
434
    private function downloadContent($url): string
435
    {
436
        $request = new Request($url, 'GET');
437
        $response = $this->client->sendRequest($request);
438
        if (200 !== $response->getStatusCode()) {
439
            throw new \InvalidArgumentException();
440
        }
441
442
        $content = $response->getBody()->getContents();
443
        if (!is_string($content)) {
444
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_URI, 'Unable to get content.');
445
        }
446
447
        return $content;
448
    }
449
450
    /**
451
     * @param array $params
452
     *
453
     * @throws OAuth2Exception
454
     *
455
     * @return Client
456
     */
457
    private function getClient(array $params): Client
458
    {
459
        $client = array_key_exists('client_id', $params) ? $this->clientRepository->find(ClientId::create($params['client_id'])) : null;
460
        if (!$client instanceof Client || true === $client->isDeleted()) {
461
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, 'Parameter "client_id" missing or invalid.');
462
        }
463
464
        return $client;
465
    }
466
467
    /**
468
     * @param Client $client
469
     *
470
     * @return JWKSet
471
     */
472
    private function getClientKeySet(Client $client): JWKSet
473
    {
474
        switch (true) {
475
            case $client->has('jwks') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod():
476
                return JWKSet::createFromJson($client->get('jwks'));
477
            case $client->has('client_secret') && 'client_secret_jwt' === $client->getTokenEndpointAuthenticationMethod():
478
                $jwk = JWK::create([
479
                    'kty' => 'oct',
480
                    'use' => 'sig',
481
                    'k' => Base64Url::encode($client->get('client_secret')),
482
                ]);
483
484
                return JWKSet::createFromKeys([$jwk]);
485
            case $client->has('jwks_uri') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod() && null !== $this->jkuFactory:
486
                return $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
487
            default:
488
                throw new \InvalidArgumentException('The client has no key or key set.');
489
        }
490
    }
491
}
492