Failed Conditions
Push — master ( 7f2d83...323120 )
by Florent
05:07
created

enableEncryptedRequestObjectSupport()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 3
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\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 (\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 (\Exception $e) {
306
            if (true === $this->requireEncryption) {
307
                throw new OAuth2Error(400, OAuth2Error::ERROR_INVALID_REQUEST_OBJECT, $e->getMessage(), [], $e);
308
            }
309
310
            return $request;
311
        }
312
    }
313
314
    private function checkAlgorithms(JWS $jws, Client $client)
315
    {
316
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
317
        if ($client->has('request_object_signing_alg') && $signatureAlgorithm !== $client->get('request_object_signing_alg')) {
318
            throw new \InvalidArgumentException('Request Object signature algorithm not allowed for the client.');
319
        }
320
321
        $this->checkUsedAlgorithm($signatureAlgorithm);
322
    }
323
324
    private function checkUsedAlgorithm(string $algorithm): void
325
    {
326
        $supportedAlgorithms = $this->getSupportedSignatureAlgorithms();
327
        if (!\in_array($algorithm, $supportedAlgorithms, true)) {
328
            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)));
329
        }
330
    }
331
332
    private function downloadContent(string $url): string
333
    {
334
        $request = new Request($url, 'GET');
335
        $response = $this->client->sendRequest($request);
336
        if (200 !== $response->getStatusCode()) {
337
            throw new \InvalidArgumentException();
338
        }
339
340
        $content = $response->getBody()->getContents();
341
        if (!\is_string($content)) {
342
            throw new OAuth2Error(400, OAuth2Error::ERROR_INVALID_REQUEST_URI, 'Unable to get content.');
343
        }
344
345
        return $content;
346
    }
347
348
    private function getClient(array $params): Client
349
    {
350
        $client = \array_key_exists('client_id', $params) ? $this->clientRepository->find(new ClientId($params['client_id'])) : null;
351
        if (!$client instanceof Client || true === $client->isDeleted()) {
352
            throw new OAuth2Error(400, OAuth2Error::ERROR_INVALID_REQUEST, 'Parameter "client_id" missing or invalid.');
353
        }
354
355
        return $client;
356
    }
357
358
    private function getClientKeySet(Client $client): JWKSet
359
    {
360
        $keyset = JWKSet::createFromKeys([]);
361
        if ($client->has('jwks')) {
362
            $jwks = JWKSet::createFromJson($client->get('jwks'));
363
            foreach ($jwks as $jwk) {
364
                $keyset = $keyset->with($jwk);
365
            }
366
        }
367
        if ($client->has('client_secret')) {
368
            $jwk = JWK::create([
369
                'kty' => 'oct',
370
                'use' => 'sig',
371
                'k' => Base64Url::encode($client->get('client_secret')),
372
            ]);
373
            $keyset = $keyset->with($jwk);
374
        }
375
        if ($client->has('jwks_uri') && null !== $this->jkuFactory) {
376
            $jwks_uri = $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
377
            foreach ($jwks_uri as $jwk) {
378
                $keyset = $keyset->with($jwk);
379
            }
380
        }
381
        if (\in_array('none', $this->getSupportedSignatureAlgorithms(), true)) {
382
            $keyset = $keyset->with(JWK::create([
383
                'kty' => 'none',
384
                'alg' => 'none',
385
                'use' => 'sig',
386
            ]));
387
        }
388
389
        if (0 === $keyset->count()) {
390
            throw new \InvalidArgumentException('The client has no key or key set.');
391
        }
392
393
        return $keyset;
394
    }
395
}
396