Failed Conditions
Push — ng ( 8b0de7...c3dfca )
by Florent
06:04
created

ClientAssertionJwt::checkClientConfiguration()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 2
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\ClientAuthentication;
15
16
use Base64Url\Base64Url;
17
use Jose\Component\Checker\ClaimCheckerManager;
18
use Jose\Component\Checker\HeaderCheckerManager;
19
use Jose\Component\Core\Converter\JsonConverter;
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\DataBag\DataBag;
30
use OAuth2Framework\Component\Core\Exception\OAuth2Exception;
31
use OAuth2Framework\Component\TrustedIssuer\TrustedIssuerRepository;
32
use Psr\Http\Message\ServerRequestInterface;
33
34
class ClientAssertionJwt implements AuthenticationMethod
35
{
36
    private const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
37
38
    /**
39
     * @var JWSVerifier
40
     */
41
    private $jwsVerifier;
42
43
    /**
44
     * @var null|TrustedIssuerRepository
45
     */
46
    private $trustedIssuerRepository = null;
47
48
    /**
49
     * @var null|JKUFactory
50
     */
51
    private $jkuFactory = null;
52
53
    /**
54
     * @var null|JWELoader
55
     */
56
    private $jweLoader = null;
57
58
    /**
59
     * @var null|JWKSet
60
     */
61
    private $keyEncryptionKeySet = null;
62
63
    /**
64
     * @var bool
65
     */
66
    private $encryptionRequired = false;
67
68
    /**
69
     * @var int
70
     */
71
    private $secretLifetime;
72
73
    /**
74
     * @var HeaderCheckerManager
75
     */
76
    private $headerCheckerManager;
77
78
    /**
79
     * @var ClaimCheckerManager
80
     */
81
    private $claimCheckerManager;
82
83
    /**
84
     * @var JsonConverter
85
     */
86
    private $jsonConverter;
87
88
    /**
89
     * ClientAssertionJwt constructor.
90
     *
91
     * @param JsonConverter        $jsonConverter
92
     * @param JWSVerifier          $jwsVerifier
93
     * @param HeaderCheckerManager $headerCheckerManager
94
     * @param ClaimCheckerManager  $claimCheckerManager
95
     * @param int                  $secretLifetime
96
     */
97
    public function __construct(JsonConverter $jsonConverter, JWSVerifier $jwsVerifier, HeaderCheckerManager $headerCheckerManager, ClaimCheckerManager $claimCheckerManager, int $secretLifetime = 0)
98
    {
99
        if ($secretLifetime < 0) {
100
            throw new \InvalidArgumentException('The secret lifetime must be at least 0 (= unlimited).');
101
        }
102
        $this->jsonConverter = $jsonConverter;
103
        $this->jwsVerifier = $jwsVerifier;
104
        $this->headerCheckerManager = $headerCheckerManager;
105
        $this->claimCheckerManager = $claimCheckerManager;
106
        $this->secretLifetime = $secretLifetime;
107
    }
108
109
    /**
110
     * @param TrustedIssuerRepository $trustedIssuerRepository
111
     */
112
    public function enableTrustedIssuerSupport(TrustedIssuerRepository $trustedIssuerRepository)
113
    {
114
        $this->trustedIssuerRepository = $trustedIssuerRepository;
115
    }
116
117
    /**
118
     * @param JKUFactory $jkuFactory
119
     */
120
    public function enableJkuSupport(JKUFactory $jkuFactory)
121
    {
122
        $this->jkuFactory = $jkuFactory;
123
    }
124
125
    /**
126
     * @param JWELoader $jweLoader
127
     * @param JWKSet    $keyEncryptionKeySet
128
     * @param bool      $encryptionRequired
129
     */
130
    public function enableEncryptedAssertions(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $encryptionRequired)
131
    {
132
        $this->jweLoader = $jweLoader;
133
        $this->encryptionRequired = $encryptionRequired;
134
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
135
    }
136
137
    /**
138
     * @return string[]
139
     */
140
    public function getSupportedSignatureAlgorithms(): array
141
    {
142
        return $this->jwsVerifier->getSignatureAlgorithmManager()->list();
143
    }
144
145
    /**
146
     * @return string[]
147
     */
148
    public function getSupportedContentEncryptionAlgorithms(): array
149
    {
150
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
151
    }
152
153
    /**
154
     * @return string[]
155
     */
156
    public function getSupportedKeyEncryptionAlgorithms(): array
157
    {
158
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function getSchemesParameters(): array
165
    {
166
        return [];
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ? ClientId
173
    {
174
        $parameters = $request->getParsedBody() ?? [];
175
        if (!array_key_exists('client_assertion_type', $parameters)) {
176
            return null;
177
        }
178
        $clientAssertionType = $parameters['client_assertion_type'];
179
180
        if (self::CLIENT_ASSERTION_TYPE !== $clientAssertionType) {
181
            return null;
182
        }
183
        if (!array_key_exists('client_assertion', $parameters)) {
184
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, 'Parameter "client_assertion" is missing.');
185
        }
186
187
        try {
188
            $client_assertion = $parameters['client_assertion'];
189
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
190
            $serializer = new CompactSerializer($this->jsonConverter);
191
            $jws = $serializer->unserialize($client_assertion);
192
            $this->headerCheckerManager->check($jws, 0);
193
            $claims = $this->jsonConverter->decode($jws->getPayload());
194
            $this->claimCheckerManager->check($claims);
195
        } catch (\Exception $e) {
196
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, 'Unable to load, decrypt or verify the client assertion.', $e);
197
        }
198
199
        // FIXME: Other claims can be considered as mandatory by the server
200
        $diff = array_diff(['iss', 'sub', 'aud', 'exp'], array_keys($claims));
201
        if (!empty($diff)) {
202
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, sprintf('The following claim(s) is/are mandatory: "%s".', implode(', ', array_values($diff))));
203
        }
204
205
        $clientCredentials = $jws;
206
207
        return ClientId::create($claims['sub']);
208
    }
209
210
    /**
211
     * @param string $assertion
212
     *
213
     * @return string
214
     *
215
     * @throws OAuth2Exception
216
     */
217
    private function tryToDecryptClientAssertion(string $assertion): string
218
    {
219
        if (null === $this->jweLoader) {
220
            return $assertion;
221
        }
222
223
        try {
224
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($assertion, $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...
225
            if (1 !== $jwe->countRecipients()) {
226
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
227
            }
228
229
            return $jwe->getPayload();
230
        } catch (\Exception $e) {
231
            if (true === $this->encryptionRequired) {
232
                throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, $e->getMessage(), $e);
233
            }
234
235
            return $assertion;
236
        }
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     */
242
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
243
    {
244
        try {
245
            if (!$clientCredentials instanceof JWS) {
0 ignored issues
show
Bug introduced by
The class Jose\Component\Signature\JWS does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
246
                return false;
247
            }
248
249
            $claims = $this->jsonConverter->decode($clientCredentials->getPayload());
250
            $jwkset = $this->retrieveIssuerKeySet($client, $clientCredentials, $claims);
251
252
            return $this->jwsVerifier->verifyWithKeySet($clientCredentials, $jwkset, 0);
253
        } catch (\Exception $e) {
254
            return false;
255
        }
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261
    public function getSupportedMethods(): array
262
    {
263
        return ['client_secret_jwt', 'private_key_jwt'];
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
270
    {
271
        switch ($commandParameters->get('token_endpoint_auth_method')) {
272
            case 'client_secret_jwt':
273
                return $this->processClientSecretJwtConfiguration($validatedParameters);
274
            case 'private_key_jwt':
275
                return $this->processPrivateKeyJwtConfiguration($commandParameters, $validatedParameters);
276
            default:
277
                return $validatedParameters;
278
        }
279
    }
280
281
    /**
282
     * @param DataBag $validatedParameters
283
     *
284
     * @return DataBag
285
     */
286
    private function processClientSecretJwtConfiguration(DataBag $validatedParameters): DataBag
287
    {
288
        $validatedParameters = $validatedParameters->with('client_secret', $this->createClientSecret());
289
        $validatedParameters = $validatedParameters->with('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : time() + $this->secretLifetime));
290
291
        return $validatedParameters;
292
    }
293
294
    /**
295
     * @param DataBag $commandParameters
296
     * @param DataBag $validatedParameters
297
     *
298
     * @return DataBag
299
     */
300
    private function processPrivateKeyJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
301
    {
302
        switch (true) {
303
            case !($commandParameters->has('jwks') xor $commandParameters->has('jwks_uri')):
304
                throw new \InvalidArgumentException('Either the parameter "jwks" or "jwks_uri" must be set.');
305
            case $commandParameters->has('jwks'):
306
                try {
307
                    JWKSet::createFromJson($commandParameters->get('jwks'));
308
                } catch (\Throwable $e) {
309
                    throw new \InvalidArgumentException('The parameter "jwks" must be a valid JWKSet object.', 0, $e);
310
                }
311
                $validatedParameters = $validatedParameters->with('jwks', $commandParameters->get('jwks'));
312
313
                break;
314
            case $commandParameters->has('jwks_uri'):
315
                if (null === $this->jkuFactory) {
316
                    throw new \InvalidArgumentException('Distant key sets cannot be used. Please use "jwks" instead of "jwks_uri".');
317
                }
318
319
                try {
320
                    $jwks = $this->jkuFactory->loadFromUrl($commandParameters->get('jwks_uri'));
321
                } catch (\Exception $e) {
322
                    throw new \InvalidArgumentException('The parameter "jwks_uri" must be a valid uri to a JWKSet.', 0, $e);
323
                }
324
                if (0 === $jwks->count()) {
325
                    throw new \InvalidArgumentException('The distant key set is empty.');
326
                }
327
                $validatedParameters = $validatedParameters->with('jwks_uri', $commandParameters->get('jwks_uri'));
328
329
                break;
330
            default:
331
                throw new \InvalidArgumentException('Either the parameter "jwks" or "jwks_uri" must be set.');
332
        }
333
334
        return $validatedParameters;
335
    }
336
337
    /**
338
     * @return string
339
     */
340
    private function createClientSecret(): string
341
    {
342
        return bin2hex(random_bytes(128));
343
    }
344
345
    /**
346
     * @param Client $client
347
     * @param JWS    $jws
348
     * @param array  $claims
349
     *
350
     * @return JWKSet
351
     */
352
    private function retrieveIssuerKeySet(Client $client, JWS $jws, array $claims): JWKSet
353
    {
354
        if ($claims['sub'] === $claims['iss']) { // The client is the issuer
355
            return $this->getClientKeySet($client);
356
        }
357
358
        if (null === $this->trustedIssuerRepository || null === $trustedIssuer = $this->trustedIssuerRepository->find($claims['iss'])) {
359
            throw new \InvalidArgumentException('Unable to retrieve the key set of the issuer.');
360
        }
361
362
        if (!in_array(self::CLIENT_ASSERTION_TYPE, $trustedIssuer->getAllowedAssertionTypes())) {
363
            throw new \InvalidArgumentException(sprintf('The assertion type "%s" is not allowed for that issuer.', self::CLIENT_ASSERTION_TYPE));
364
        }
365
366
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
367
        if (!in_array($signatureAlgorithm, $trustedIssuer->getAllowedSignatureAlgorithms())) {
368
            throw new \InvalidArgumentException(sprintf('The signature algorithm "%s" is not allowed for that issuer.', $signatureAlgorithm));
369
        }
370
371
        return $trustedIssuer->getJWKSet();
372
    }
373
374
    /**
375
     * @param Client $client
376
     *
377
     * @return JWKSet
378
     */
379
    private function getClientKeySet(Client $client): JWKSet
380
    {
381
        switch (true) {
382
            case $client->has('jwks') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod():
383
                return JWKSet::createFromJson($client->get('jwks'));
384
            case $client->has('client_secret') && 'client_secret_jwt' === $client->getTokenEndpointAuthenticationMethod():
385
                $jwk = JWK::create([
386
                    'kty' => 'oct',
387
                    'use' => 'sig',
388
                    'k' => Base64Url::encode($client->get('client_secret')),
389
                ]);
390
391
                return JWKSet::createFromKeys([$jwk]);
392
            case $client->has('jwks_uri') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod() && null !== $this->jkuFactory:
393
                return $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
394
            default:
395
                throw new \InvalidArgumentException('The client has no key or key set.');
396
        }
397
    }
398
}
399