Failed Conditions
Pull Request — master (#163)
by
unknown
12:22 queued 08:31
created

checkIssuerAndClientId()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 2
nc 2
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2019 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 Assert\Assertion;
17
use Base64Url\Base64Url;
18
use Jose\Component\Checker\ClaimCheckerManager;
19
use Jose\Component\Core\JWK;
20
use Jose\Component\Core\JWKSet;
21
use Jose\Component\Core\Util\JsonConverter;
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\Client\ClientInterface;
32
use Psr\Http\Message\RequestFactoryInterface;
33
use Safe\Exceptions\PcreException;
34
use function Safe\preg_match;
35
use function Safe\sprintf;
36
37
class AuthorizationRequestLoader
38
{
39
    /**
40
     * @var ClientRepository
41
     */
42
    private $clientRepository;
43
44
    /**
45
     * @var bool
46
     */
47
    private $requestObjectAllowed = false;
48
49
    /**
50
     * @var bool
51
     */
52
    private $requestObjectReferenceAllowed = false;
53
54
    /**
55
     * @var JWKSet
56
     */
57
    private $keyEncryptionKeySet;
58
59
    /**
60
     * @var bool
61
     */
62
    private $requireRequestUriRegistration = true;
63
64
    /**
65
     * @var bool
66
     */
67
    private $requireEncryption = false;
68
69
    /**
70
     * @var null|ClientInterface
71
     */
72
    private $client;
73
74
    /**
75
     * @var JWSVerifier
76
     */
77
    private $jwsVerifier;
78
79
    /**
80
     * @var ClaimCheckerManager
81
     */
82
    private $claimCheckerManager;
83
84
    /**
85
     * @var JWELoader
86
     */
87
    private $jweLoader;
88
89
    /**
90
     * @var null|JKUFactory
91
     */
92
    private $jkuFactory;
93
94
    /**
95
     * @var RequestFactoryInterface
96
     */
97
    private $requestFactory;
98
99
    public function __construct(ClientRepository $clientRepository)
100
    {
101
        $this->clientRepository = $clientRepository;
102
    }
103
104
    public function isRequestUriRegistrationRequired(): bool
105
    {
106
        return $this->requireRequestUriRegistration;
107
    }
108
109
    public function isRequestObjectSupportEnabled(): bool
110
    {
111
        return $this->requestObjectAllowed;
112
    }
113
114
    public function isRequestObjectReferenceSupportEnabled(): bool
115
    {
116
        return $this->requestObjectReferenceAllowed;
117
    }
118
119
    /**
120
     * @return string[]
121
     */
122
    public function getSupportedSignatureAlgorithms(): array
123
    {
124
        return null === $this->jwsVerifier ? [] : $this->jwsVerifier->getSignatureAlgorithmManager()->list();
125
    }
126
127
    /**
128
     * @return string[]
129
     */
130
    public function getSupportedKeyEncryptionAlgorithms(): array
131
    {
132
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
133
    }
134
135
    /**
136
     * @return string[]
137
     */
138
    public function getSupportedContentEncryptionAlgorithms(): array
139
    {
140
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
141
    }
142
143
    public function enableSignedRequestObjectSupport(JWSVerifier $jwsVerifier, ClaimCheckerManager $claimCheckerManager): void
144
    {
145
        $this->jwsVerifier = $jwsVerifier;
146
        $this->claimCheckerManager = $claimCheckerManager;
147
        $this->requestObjectAllowed = true;
148
    }
149
150
    public function enableRequestObjectReferenceSupport(ClientInterface $client, RequestFactoryInterface $requestFactory, bool $requireRequestUriRegistration): void
151
    {
152
        Assertion::true($this->isRequestObjectSupportEnabled(), 'Request object support must be enabled first.');
153
        $this->requestObjectReferenceAllowed = true;
154
        $this->requireRequestUriRegistration = $requireRequestUriRegistration;
155
        $this->requestFactory = $requestFactory;
156
        $this->client = $client;
157
    }
158
159
    public function enableEncryptedRequestObjectSupport(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $requireEncryption): void
160
    {
161
        Assertion::true($this->isRequestObjectSupportEnabled(), 'Request object support must be enabled first.');
162
        Assertion::greaterThan($keyEncryptionKeySet->count(), 0, 'The encryption key set must have at least one key.');
163
        $this->jweLoader = $jweLoader;
164
        $this->requireEncryption = $requireEncryption;
165
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
166
    }
167
168
    public function enableJkuSupport(JKUFactory $jkuFactory): void
169
    {
170
        $this->jkuFactory = $jkuFactory;
171
    }
172
173
    public function isEncryptedRequestSupportEnabled(): bool
174
    {
175
        return null !== $this->keyEncryptionKeySet;
176
    }
177
178
    /**
179
     * @throws OAuth2Error
180
     * @throws PcreException
181
     */
182
    public function load(array $queryParameters): AuthorizationRequest
183
    {
184
        $client = null;
185
        if (\array_key_exists('request', $queryParameters)) {
186
            $queryParameters = $this->createFromRequestParameter($queryParameters, $client);
187
        } elseif (\array_key_exists('request_uri', $queryParameters)) {
188
            $queryParameters = $this->createFromRequestUriParameter($queryParameters, $client);
189
        } else {
190
            $client = $this->getClient($queryParameters);
191
        }
192
193
        return new AuthorizationRequest($client, $queryParameters);
0 ignored issues
show
Bug introduced by
It seems like $client can also be of type null; however, parameter $client of OAuth2Framework\Componen...nRequest::__construct() does only seem to accept OAuth2Framework\Component\Core\Client\Client, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

193
        return new AuthorizationRequest(/** @scrutinizer ignore-type */ $client, $queryParameters);
Loading history...
194
    }
195
196
    /**
197
     * @throws OAuth2Error
198
     */
199
    private function createFromRequestParameter(array $params, Client &$client = null): array
200
    {
201
        if (false === $this->isRequestObjectSupportEnabled()) {
202
            throw OAuth2Error::requestNotSupported('The parameter "request" is not supported.');
203
        }
204
        $request = $params['request'];
205
        if (!\is_string($request)) {
206
            throw OAuth2Error::requestNotSupported('The parameter "request" must be an assertion.');
207
        }
208
209
        $params = $this->loadRequestObject($params, $request, $client);
210
        $this->checkIssuerAndClientId($params);
211
212
        return $params;
213
    }
214
215
    /**
216
     * @throws OAuth2Error
217
     * @throws PcreException
218
     */
219
    private function createFromRequestUriParameter(array $params, Client &$client = null): array
220
    {
221
        if (false === $this->isRequestObjectReferenceSupportEnabled()) {
222
            throw OAuth2Error::requestUriNotSupported('The parameter "request_uri" is not supported.');
223
        }
224
        $requestUri = $params['request_uri'];
225
        if (1 === preg_match('#/\.\.?(/|$)#', $requestUri)) {
226
            throw OAuth2Error::invalidRequestUri('The request Uri is not allowed.');
227
        }
228
        $content = $this->downloadContent($requestUri);
229
        $params = $this->loadRequestObject($params, $content, $client);
230
        $this->checkRequestUri($client, $requestUri);
231
        $this->checkIssuerAndClientId($params);
232
233
        return $params;
234
    }
235
236
    /**
237
     * @param array $params
238
     */
239
    private function checkIssuerAndClientId(array $params): void
240
    {
241
        if (\array_key_exists('iss', $params) && \array_key_exists('client_id', $params)) {
242
            Assertion::eq($params['iss'], $params['client_id'], 'The issuer of the request object is not the client who requests the authorization.');
243
        }
244
    }
245
246
    /**
247
     * @throws OAuth2Error
248
     */
249
    private function checkRequestUri(Client $client, string $requestUri): void
250
    {
251
        $storedRequestUris = $client->has('request_uris') ? $client->get('request_uris') : [];
252
        if (empty($storedRequestUris)) {
253
            if ($this->isRequestUriRegistrationRequired()) {
254
                throw OAuth2Error::invalidRequestUri('The clients shall register at least one request object uri.');
255
            }
256
257
            return;
258
        }
259
260
        foreach ($storedRequestUris as $storedRequestUri) {
261
            if (0 === strcasecmp(mb_substr($requestUri, 0, mb_strlen($storedRequestUri, '8bit'), '8bit'), $storedRequestUri)) {
262
                return;
263
            }
264
        }
265
266
        throw OAuth2Error::invalidRequestUri('The request Uri is not allowed.');
267
    }
268
269
    /**
270
     * @throws OAuth2Error
271
     */
272
    private function loadRequestObject(array $params, string $request, Client &$client = null): array
273
    {
274
        // FIXME Can be
275
        // - encrypted (not supported)
276
        // - encrypted and signed (supported)
277
        // - signed (supported)
278
        $request = $this->tryToLoadEncryptedRequest($request);
279
280
        try {
281
            $serializer = new CompactSerializer();
282
            $jwt = $serializer->unserialize($request);
283
284
            $claims = JsonConverter::decode(
285
                $jwt->getPayload()
0 ignored issues
show
Bug introduced by
It seems like $jwt->getPayload() can also be of type null; however, parameter $payload of Jose\Component\Core\Util\JsonConverter::decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

285
                /** @scrutinizer ignore-type */ $jwt->getPayload()
Loading history...
286
            );
287
            Assertion::isArray($claims, 'Invalid assertion. The payload must contain claims.');
288
            $this->claimCheckerManager->check($claims);
289
            $parameters = array_merge($params, $claims);
290
            $client = $this->getClient($parameters);
291
292
            $public_key_set = $this->getClientKeySet($client);
293
            $this->checkAlgorithms($jwt, $client);
294
            Assertion::true($this->jwsVerifier->verifyWithKeySet($jwt, $public_key_set, 0), 'The verification of the request object failed.'); //FIXME: header checker should be used
295
296
            return $parameters;
297
        } catch (OAuth2Error $e) {
298
            throw $e;
299
        } catch (\Throwable $e) {
300
            throw OAuth2Error::invalidRequestObject($e->getMessage(), [], $e);
301
        }
302
    }
303
304
    /**
305
     * @throws OAuth2Error
306
     */
307
    private function tryToLoadEncryptedRequest(string $request): string
308
    {
309
        if (null === $this->jweLoader) {
310
            return $request;
311
        }
312
313
        try {
314
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($request, $this->keyEncryptionKeySet, $recipient);
315
            Assertion::eq(1, $jwe->countRecipients(), 'The request must use the compact serialization mode.');
316
317
            return $jwe->getPayload();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $jwe->getPayload() could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
318
        } catch (OAuth2Error $e) {
319
            throw $e;
320
        } catch (\Throwable $e) {
321
            if (true === $this->requireEncryption) {
322
                throw OAuth2Error::invalidRequestObject($e->getMessage(), [], $e);
323
            }
324
325
            return $request;
326
        }
327
    }
328
329
    private function checkAlgorithms(JWS $jws, Client $client): void
330
    {
331
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
332
        Assertion::string($signatureAlgorithm, 'Invalid algorithm parameter in Request Object.');
333
        if ($client->has('request_object_signing_alg')) {
334
            Assertion::eq($signatureAlgorithm, $client->get('request_object_signing_alg'), 'Request Object signature algorithm not allowed for the client.');
335
        }
336
337
        $this->checkUsedAlgorithm($signatureAlgorithm);
338
    }
339
340
    private function checkUsedAlgorithm(string $algorithm): void
341
    {
342
        $supportedAlgorithms = $this->getSupportedSignatureAlgorithms();
343
        Assertion::inArray($algorithm, $supportedAlgorithms, sprintf('The algorithm "%s" is not allowed for request object signatures. Please use one of the following algorithm(s): %s', $algorithm, implode(', ', $supportedAlgorithms)));
344
    }
345
346
    private function downloadContent(string $url): string
347
    {
348
        $request = $this->requestFactory->createRequest('GET', $url);
349
        $response = $this->client->sendRequest($request);
0 ignored issues
show
Bug introduced by
The method sendRequest() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

349
        /** @scrutinizer ignore-call */ 
350
        $response = $this->client->sendRequest($request);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
350
        Assertion::eq(200, $response->getStatusCode(), 'Unable to load the request object');
351
352
        return $response->getBody()->getContents();
353
    }
354
355
    /**
356
     * @throws OAuth2Error
357
     */
358
    private function getClient(array $params): Client
359
    {
360
        $client = \array_key_exists('client_id', $params) ? $this->clientRepository->find(new ClientId($params['client_id'])) : null;
361
        if (!$client instanceof Client || true === $client->isDeleted()) {
362
            throw OAuth2Error::invalidRequest('Parameter "client_id" missing or invalid.');
363
        }
364
365
        return $client;
366
    }
367
368
    private function getClientKeySet(Client $client): JWKSet
369
    {
370
        $keyset = new JWKSet([]);
371
        if ($client->has('jwks')) {
372
            $jwks = JWKSet::createFromJson($client->get('jwks'));
0 ignored issues
show
Bug introduced by
It seems like $client->get('jwks') can also be of type null; however, parameter $json of Jose\Component\Core\JWKSet::createFromJson() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

372
            $jwks = JWKSet::createFromJson(/** @scrutinizer ignore-type */ $client->get('jwks'));
Loading history...
373
            foreach ($jwks as $jwk) {
374
                $keyset = $keyset->with($jwk);
375
            }
376
        }
377
        if ($client->has('client_secret')) {
378
            $jwk = new JWK([
379
                'kty' => 'oct',
380
                'use' => 'sig',
381
                'k' => Base64Url::encode($client->get('client_secret')),
0 ignored issues
show
Bug introduced by
It seems like $client->get('client_secret') can also be of type null; however, parameter $data of Base64Url\Base64Url::encode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

381
                'k' => Base64Url::encode(/** @scrutinizer ignore-type */ $client->get('client_secret')),
Loading history...
382
            ]);
383
            $keyset = $keyset->with($jwk);
384
        }
385
        if ($client->has('jwks_uri') && null !== $this->jkuFactory) {
386
            $jwks_uri = $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
0 ignored issues
show
Bug introduced by
It seems like $client->get('jwks_uri') can also be of type null; however, parameter $url of Jose\Component\KeyManage...UFactory::loadFromUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

386
            $jwks_uri = $this->jkuFactory->loadFromUrl(/** @scrutinizer ignore-type */ $client->get('jwks_uri'));
Loading history...
387
            foreach ($jwks_uri as $jwk) {
388
                $keyset = $keyset->with($jwk);
389
            }
390
        }
391
        if (\in_array('none', $this->getSupportedSignatureAlgorithms(), true)) {
392
            $keyset = $keyset->with(new JWK([
393
                'kty' => 'none',
394
                'alg' => 'none',
395
                'use' => 'sig',
396
            ]));
397
        }
398
399
        Assertion::greaterThan($keyset->count(), 0, 'The client has no key or key set.');
400
401
        return $keyset;
402
    }
403
}
404