Failed Conditions
Push — master ( 14a869...357c66 )
by Florent
05:07
created

ClientAssertionJwt::enableJkuSupport()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
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\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\Message\OAuth2Message;
31
use OAuth2Framework\Component\Core\TrustedIssuer\TrustedIssuerRepository;
32
use OAuth2Framework\Component\Core\Util\RequestBodyParser;
33
use Psr\Http\Message\ServerRequestInterface;
34
35
class ClientAssertionJwt implements AuthenticationMethod
36
{
37
    private const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
38
39
    /**
40
     * @var JWSVerifier
41
     */
42
    private $jwsVerifier;
43
44
    /**
45
     * @var null|TrustedIssuerRepository
46
     */
47
    private $trustedIssuerRepository = null;
48
49
    /**
50
     * @var null|JKUFactory
51
     */
52
    private $jkuFactory = null;
53
54
    /**
55
     * @var null|JWELoader
56
     */
57
    private $jweLoader = null;
58
59
    /**
60
     * @var null|JWKSet
61
     */
62
    private $keyEncryptionKeySet = null;
63
64
    /**
65
     * @var bool
66
     */
67
    private $encryptionRequired = false;
68
69
    /**
70
     * @var int
71
     */
72
    private $secretLifetime;
73
74
    /**
75
     * @var HeaderCheckerManager
76
     */
77
    private $headerCheckerManager;
78
79
    /**
80
     * @var ClaimCheckerManager
81
     */
82
    private $claimCheckerManager;
83
84
    /**
85
     * @var JsonConverter
86
     */
87
    private $jsonConverter;
88
89
    /**
90
     * ClientAssertionJwt constructor.
91
     *
92
     * @param JsonConverter        $jsonConverter
93
     * @param JWSVerifier          $jwsVerifier
94
     * @param HeaderCheckerManager $headerCheckerManager
95
     * @param ClaimCheckerManager  $claimCheckerManager
96
     * @param int                  $secretLifetime
97
     */
98
    public function __construct(JsonConverter $jsonConverter, JWSVerifier $jwsVerifier, HeaderCheckerManager $headerCheckerManager, ClaimCheckerManager $claimCheckerManager, int $secretLifetime = 0)
99
    {
100
        if ($secretLifetime < 0) {
101
            throw new \InvalidArgumentException('The secret lifetime must be at least 0 (= unlimited).');
102
        }
103
        $this->jsonConverter = $jsonConverter;
104
        $this->jwsVerifier = $jwsVerifier;
105
        $this->headerCheckerManager = $headerCheckerManager;
106
        $this->claimCheckerManager = $claimCheckerManager;
107
        $this->secretLifetime = $secretLifetime;
108
    }
109
110
    /**
111
     * @param TrustedIssuerRepository $trustedIssuerRepository
112
     */
113
    public function enableTrustedIssuerSupport(TrustedIssuerRepository $trustedIssuerRepository)
114
    {
115
        $this->trustedIssuerRepository = $trustedIssuerRepository;
116
    }
117
118
    /**
119
     * @param JKUFactory $jkuFactory
120
     */
121
    public function enableJkuSupport(JKUFactory $jkuFactory)
122
    {
123
        $this->jkuFactory = $jkuFactory;
124
    }
125
126
    /**
127
     * @param JWELoader $jweLoader
128
     * @param JWKSet    $keyEncryptionKeySet
129
     * @param bool      $encryptionRequired
130
     */
131
    public function enableEncryptedAssertions(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $encryptionRequired)
132
    {
133
        $this->jweLoader = $jweLoader;
134
        $this->encryptionRequired = $encryptionRequired;
135
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
136
    }
137
138
    /**
139
     * @return string[]
140
     */
141
    public function getSupportedSignatureAlgorithms(): array
142
    {
143
        return $this->jwsVerifier->getSignatureAlgorithmManager()->list();
144
    }
145
146
    /**
147
     * @return string[]
148
     */
149
    public function getSupportedContentEncryptionAlgorithms(): array
150
    {
151
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
152
    }
153
154
    /**
155
     * @return string[]
156
     */
157
    public function getSupportedKeyEncryptionAlgorithms(): array
158
    {
159
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function getSchemesParameters(): array
166
    {
167
        return [];
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ? ClientId
174
    {
175
        $parameters = RequestBodyParser::parseFormUrlEncoded($request);
176
        if (!array_key_exists('client_assertion_type', $parameters)) {
177
            return null;
178
        }
179
        $clientAssertionType = $parameters['client_assertion_type'];
180
181
        if (self::CLIENT_ASSERTION_TYPE !== $clientAssertionType) {
182
            return null;
183
        }
184
        if (!array_key_exists('client_assertion', $parameters)) {
185
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, 'Parameter "client_assertion" is missing.');
186
        }
187
188
        try {
189
            $client_assertion = $parameters['client_assertion'];
190
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
191
            $serializer = new CompactSerializer($this->jsonConverter);
192
            $jws = $serializer->unserialize($client_assertion);
193
            $this->headerCheckerManager->check($jws, 0);
194
            $claims = $this->jsonConverter->decode($jws->getPayload());
195
            $this->claimCheckerManager->check($claims);
196
        } catch (OAuth2Message $e) {
197
            throw $e;
198
        } catch (\Exception $e) {
199
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, 'Unable to load, decrypt or verify the client assertion.', [], $e);
200
        }
201
202
        // FIXME: Other claims can be considered as mandatory by the server
203
        $diff = array_diff(['iss', 'sub', 'aud', 'exp'], array_keys($claims));
204
        if (!empty($diff)) {
205
            throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, sprintf('The following claim(s) is/are mandatory: "%s".', implode(', ', array_values($diff))));
206
        }
207
208
        $clientCredentials = $jws;
209
210
        return ClientId::create($claims['sub']);
211
    }
212
213
    /**
214
     * @param string $assertion
215
     *
216
     * @return string
217
     *
218
     * @throws OAuth2Message
219
     */
220
    private function tryToDecryptClientAssertion(string $assertion): string
221
    {
222
        if (null === $this->jweLoader) {
223
            return $assertion;
224
        }
225
226
        try {
227
            $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...
228
            if (1 !== $jwe->countRecipients()) {
229
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
230
            }
231
232
            return $jwe->getPayload();
233
        } catch (\Exception $e) {
234
            if (true === $this->encryptionRequired) {
235
                throw new OAuth2Message(400, OAuth2Message::ERROR_INVALID_REQUEST, 'The encryption of the assertion is mandatory but the decryption of the assertion failed.', [], $e);
236
            }
237
238
            return $assertion;
239
        }
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
246
    {
247
        try {
248
            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...
249
                return false;
250
            }
251
252
            $claims = $this->jsonConverter->decode($clientCredentials->getPayload());
253
            $jwkset = $this->retrieveIssuerKeySet($client, $clientCredentials, $claims);
254
255
            return $this->jwsVerifier->verifyWithKeySet($clientCredentials, $jwkset, 0);
256
        } catch (\Exception $e) {
257
            return false;
258
        }
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     */
264
    public function getSupportedMethods(): array
265
    {
266
        return ['client_secret_jwt', 'private_key_jwt'];
267
    }
268
269
    /**
270
     * {@inheritdoc}
271
     */
272
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
273
    {
274
        switch ($commandParameters->get('token_endpoint_auth_method')) {
275
            case 'client_secret_jwt':
276
                return $this->checkClientSecretJwtConfiguration($commandParameters, $validatedParameters);
277
            case 'private_key_jwt':
278
                return $this->checkPrivateKeyJwtConfiguration($commandParameters, $validatedParameters);
279
            default:
280
                return $validatedParameters;
281
        }
282
    }
283
284
    /**
285
     * @param DataBag $commandParameters
286
     * @param DataBag $validatedParameters
287
     *
288
     * @return DataBag
289
     */
290
    private function checkClientSecretJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
291
    {
292
        $validatedParameters->with('token_endpoint_auth_method', $commandParameters->get('token_endpoint_auth_method'));
293
        $validatedParameters->with('client_secret', $this->createClientSecret());
294
        $validatedParameters->with('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : time() + $this->secretLifetime));
295
296
        return $validatedParameters;
297
    }
298
299
    /**
300
     * @param DataBag $commandParameters
301
     * @param DataBag $validatedParameters
302
     *
303
     * @return DataBag
304
     */
305
    private function checkPrivateKeyJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
306
    {
307
        switch (true) {
308
            case $commandParameters->has('jwks') && $commandParameters->has('jwks_uri'):
309
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null === $this->trustedIssuerRepository:
310
                throw new \InvalidArgumentException('Either the parameter "jwks" or "jwks_uri" must be set.');
311
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null !== $this->trustedIssuerRepository: //Allowed when trusted issuer support is set
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
312
313
                break;
314
            case $commandParameters->has('jwks'):
315
                $validatedParameters->with('jwks', $commandParameters->get('jwks'));
316
317
                break;
318
            case $commandParameters->has('jwks_uri'):
319
                $validatedParameters->with('jwks_uri', $commandParameters->get('jwks_uri'));
320
321
                break;
322
        }
323
324
        return $validatedParameters;
325
    }
326
327
    /**
328
     * @return string
329
     */
330
    private function createClientSecret(): string
331
    {
332
        return bin2hex(random_bytes(32));
333
    }
334
335
    /**
336
     * @param Client $client
337
     * @param JWS    $jws
338
     * @param array  $claims
339
     *
340
     * @return JWKSet
341
     */
342
    private function retrieveIssuerKeySet(Client $client, JWS $jws, array $claims): JWKSet
343
    {
344
        if ($claims['sub'] === $claims['iss']) { // The client is the issuer
345
            return $this->getClientKeySet($client);
346
        }
347
348
        if (null === $this->trustedIssuerRepository || null === $trustedIssuer = $this->trustedIssuerRepository->find($claims['iss'])) {
349
            throw new \InvalidArgumentException('Unable to retrieve the key set of the issuer.');
350
        }
351
352
        if (!in_array(self::CLIENT_ASSERTION_TYPE, $trustedIssuer->getAllowedAssertionTypes())) {
353
            throw new \InvalidArgumentException(sprintf('The assertion type "%s" is not allowed for that issuer.', self::CLIENT_ASSERTION_TYPE));
354
        }
355
356
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
357
        if (!in_array($signatureAlgorithm, $trustedIssuer->getAllowedSignatureAlgorithms())) {
358
            throw new \InvalidArgumentException(sprintf('The signature algorithm "%s" is not allowed for that issuer.', $signatureAlgorithm));
359
        }
360
361
        return $trustedIssuer->getJWKSet();
362
    }
363
364
    /**
365
     * @param Client $client
366
     *
367
     * @return JWKSet
368
     */
369
    private function getClientKeySet(Client $client): JWKSet
370
    {
371
        switch (true) {
372
            case $client->has('jwks') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod():
373
                $jwks = json_decode(json_encode($client->get('jwks'), JSON_FORCE_OBJECT), true);
374
375
                return JWKSet::createFromKeyData($jwks);
376
            case $client->has('client_secret') && 'client_secret_jwt' === $client->getTokenEndpointAuthenticationMethod():
377
                $jwk = JWK::create([
378
                    'kty' => 'oct',
379
                    'use' => 'sig',
380
                    'k' => Base64Url::encode($client->get('client_secret')),
381
                ]);
382
383
                return JWKSet::createFromKeys([$jwk]);
384
            case $client->has('jwks_uri') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod() && null !== $this->jkuFactory:
385
                return $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
386
            default:
387
                throw new \InvalidArgumentException('The client has no key or key set.');
388
        }
389
    }
390
}
391