Failed Conditions
Push — master ( 7c3864...930f9b )
by Florent
14:15
created

AuthorizationRequestLoader::downloadContent()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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