Failed Conditions
Push — ng ( 40bc56...6c4ccd )
by Florent
12:27 queued 08:41
created

enableEncryptedRequestObjectSupport()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
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 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\Core\Client\Client;
23
use OAuth2Framework\Component\Core\Client\ClientId;
24
use OAuth2Framework\Component\Core\Client\ClientRepository;
25
use OAuth2Framework\Component\Core\Exception\OAuth2Exception;
26
use Psr\Http\Message\ServerRequestInterface;
27
use Zend\Diactoros\Request;
28
29
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();
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);
374
            $this->claimCheckerManager->check($jwt);
375
            $claims = json_decode($jwt->getPayload(), true);
376
377
            $client = $this->getClient(array_merge($params, $claims));
378
            $public_key_set = $client->getPublicKeySet();
379
            if (null === $public_key_set) {
380
                throw new \InvalidArgumentException('The client does not have signature capabilities.');
381
            }
382
            $index = $this->jwsLoader->verifyWithKeySet($jwt, $public_key_set);
383
            $this->checkAlgorithms($jwt, $index, $client);
384
            $missing_claims = array_keys(array_diff_key(array_flip($this->mandatoryClaims), $claims));
385
            if (!empty($missing_claims)) {
386
                throw new \InvalidArgumentException(sprintf('The following mandatory claims are missing: %s.', implode(', ', $missing_claims)));
387
            }
388
        } catch (\Exception $e) {
389
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), $e);
390
        }
391
392
        return $jwt;
393
    }
394
395
    /**
396
     * @param string $request
397
     *
398
     * @return string
399
     *
400
     * @throws OAuth2Exception
401
     */
402
    private function tryToLoadEncryptedRequest(string $request): string
403
    {
404
        if (null === $this->jweLoader) {
405
            return $request;
406
        }
407
408
        try {
409
            $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...
410
            if (1 !== $jwe->countRecipients()) {
411
                throw new \InvalidArgumentException('The request must use the compact serialization mode.');
412
            }
413
414
            return $jwe->getPayload();
415
        } catch (\Exception $e) {
416
            if (true === $this->requireEncryption) {
417
                throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), $e);
418
            }
419
420
            return $request;
421
        }
422
    }
423
424
    /**
425
     * @param JWS    $jws
426
     * @param int    $index
427
     * @param Client $client
428
     *
429
     * @throws \InvalidArgumentException
430
     */
431
    private function checkAlgorithms(JWS $jws, int $index, Client $client)
432
    {
433
        if (!$client->has('request_object_signing_alg')) {
434
            throw new \InvalidArgumentException('Request Object signature algorithm not defined for the client.');
435
        }
436
        if ($jws->getSignature($index)->getProtectedHeaderParameter('alg') !== $client->get('request_object_signing_alg')) {
437
            throw new \InvalidArgumentException('Request Object signature algorithm not supported by the client.');
438
        }
439
    }
440
441
    /**
442
     * @param $url
443
     *
444
     * @return string
445
     *
446
     * @throws OAuth2Exception
447
     * @throws \Exception
448
     * @throws \Http\Client\Exception
449
     */
450
    private function downloadContent($url): string
451
    {
452
        $request = new Request($url, 'GET');
453
        $response = $this->client->sendRequest($request);
454
        if (200 !== $response->getStatusCode()) {
455
            throw new \InvalidArgumentException();
456
        }
457
458
        $content = $response->getBody()->getContents();
459
        if (!is_string($content)) {
460
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST_URI, 'Unable to get content.');
461
        }
462
463
        return $content;
464
    }
465
466
    /**
467
     * @param array $params
468
     *
469
     * @throws OAuth2Exception
470
     *
471
     * @return Client
472
     */
473
    private function getClient(array $params): Client
474
    {
475
        $client = array_key_exists('client_id', $params) ? $this->clientRepository->find(ClientId::create($params['client_id'])) : null;
476
        if (!$client instanceof Client || true === $client->isDeleted()) {
477
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, 'Parameter "client_id" missing or invalid.');
478
        }
479
480
        return $client;
481
    }
482
}
483