Failed Conditions
Push — ng ( 941bba...49c420 )
by Florent
14:14
created

getSupportedSignatureAlgorithms()   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 0
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\Bundle\Tests\TestBundle\Entity\TrustedIssuerRepository;
28
use OAuth2Framework\Component\Core\Client\Client;
29
use OAuth2Framework\Component\Core\Client\ClientId;
30
use OAuth2Framework\Component\Core\DataBag\DataBag;
31
use OAuth2Framework\Component\Core\Exception\OAuth2Exception;
32
use Psr\Http\Message\ServerRequestInterface;
33
34
class ClientAssertionJwt implements AuthenticationMethod
35
{/**
0 ignored issues
show
Coding Style introduced by
The opening class brace should be on a newline by itself.
Loading history...
36
 * @var JWSVerifier
37
 */
38
    private $jwsVerifier;
39
40
    /**
41
     * @var null|TrustedIssuerRepository
42
     */
43
    private $trustedIssuerRepository = null;
44
45
    /**
46
     * @var null|JKUFactory
47
     */
48
    private $jkuFactory = null;
49
50
    /**
51
     * @var null|JWELoader
52
     */
53
    private $jweLoader = null;
54
55
    /**
56
     * @var null|JWKSet
57
     */
58
    private $keyEncryptionKeySet = null;
59
60
    /**
61
     * @var bool
62
     */
63
    private $encryptionRequired = false;
64
65
    /**
66
     * @var int
67
     */
68
    private $secretLifetime;
69
70
    /**
71
     * @var HeaderCheckerManager
72
     */
73
    private $headerCheckerManager;
74
75
    /**
76
     * @var ClaimCheckerManager
77
     */
78
    private $claimCheckerManager;
79
80
    /**
81
     * @var JsonConverter
82
     */
83
    private $jsonConverter;
84
85
    /**
86
     * ClientAssertionJwt constructor.
87
     *
88
     * @param JsonConverter        $jsonConverter
89
     * @param JWSVerifier          $jwsVerifier
90
     * @param HeaderCheckerManager $headerCheckerManager
91
     * @param ClaimCheckerManager  $claimCheckerManager
92
     * @param int                  $secretLifetime
93
     */
94
    public function __construct(JsonConverter $jsonConverter, JWSVerifier $jwsVerifier, HeaderCheckerManager $headerCheckerManager, ClaimCheckerManager $claimCheckerManager, int $secretLifetime = 0)
95
    {
96
        if ($secretLifetime < 0) {
97
            throw new \InvalidArgumentException('The secret lifetime must be at least 0 (= unlimited).');
98
        }
99
        $this->jsonConverter = $jsonConverter;
100
        $this->jwsVerifier = $jwsVerifier;
101
        $this->headerCheckerManager = $headerCheckerManager;
102
        $this->claimCheckerManager = $claimCheckerManager;
103
        $this->secretLifetime = $secretLifetime;
104
    }
105
106
    /**
107
     * @param TrustedIssuerRepository $trustedIssuerRepository
108
     */
109
    public function enableTrustedIssuerSupport(TrustedIssuerRepository $trustedIssuerRepository)
110
    {
111
        $this->trustedIssuerRepository = $trustedIssuerRepository;
112
    }
113
114
    /**
115
     * @param JKUFactory $jkuFactory
116
     */
117
    public function enableJkuSupport(JKUFactory $jkuFactory)
118
    {
119
        $this->jkuFactory = $jkuFactory;
120
    }
121
122
    /**
123
     * @param JWELoader $jweLoader
124
     * @param JWKSet    $keyEncryptionKeySet
125
     * @param bool      $encryptionRequired
126
     */
127
    public function enableEncryptedAssertions(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $encryptionRequired)
128
    {
129
        $this->jweLoader = $jweLoader;
130
        $this->encryptionRequired = $encryptionRequired;
131
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
132
    }
133
134
    /**
135
     * @return string[]
136
     */
137
    public function getSupportedSignatureAlgorithms(): array
138
    {
139
        return $this->jwsVerifier->getSignatureAlgorithmManager()->list();
140
    }
141
142
    /**
143
     * @return string[]
144
     */
145
    public function getSupportedContentEncryptionAlgorithms(): array
146
    {
147
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
148
    }
149
150
    /**
151
     * @return string[]
152
     */
153
    public function getSupportedKeyEncryptionAlgorithms(): array
154
    {
155
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function getSchemesParameters(): array
162
    {
163
        return [];
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ? ClientId
170
    {
171
        $parameters = $request->getParsedBody() ?? [];
172
        if (!array_key_exists('client_assertion_type', $parameters)) {
173
            return null;
174
        }
175
        $clientAssertionType = $parameters['client_assertion_type'];
176
177
        if ('urn:ietf:params:oauth:client-assertion-type:jwt-bearer' !== $clientAssertionType) {
178
            return null;
179
        }
180
181
        try {
182
            if (!array_key_exists('client_assertion', $parameters)) {
183
                throw new \InvalidArgumentException('Parameter "client_assertion" is missing.');
184
            }
185
            $client_assertion = $parameters['client_assertion'];
186
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
187
            $serializer = new CompactSerializer($this->jsonConverter);
188
            $jws = $serializer->unserialize($client_assertion);
189
            if (1 !== $jws->countSignatures()) {
190
                throw new \InvalidArgumentException('The assertion must have only one signature.');
191
            }
192
            $this->headerCheckerManager->check($jws, 0);
193
            $claims = $this->jsonConverter->decode($jws->getPayload());
194
            $this->claimCheckerManager->check($claims);
195
196
            // FIXME: Other claims can be considered as mandatory
197
            $diff = array_diff(['iss', 'sub', 'aud', 'exp'], array_keys($claims));
198
            if (!empty($diff)) {
199
                throw new \InvalidArgumentException(sprintf('The following claim(s) is/are mandatory: "%s".', implode(', ', array_values($diff))));
200
            }
201
202
            $clientCredentials = $jws;
203
204
            return ClientId::create($claims['sub']);
205
        } catch (\Exception $e) {
206
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, $e->getMessage(), $e);
207
        }
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
It seems like $this->keyEncryptionKeySet can be null; however, loadAndDecryptWithKeySet() 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...
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) {
246
                return false;
247
            }
248
249
            $claims = $this->jsonConverter->decode($clientCredentials->getPayload());
250
            $jwkset = $this->retrieveIssuerKeySet($client, $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
        if ('client_secret_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
272
            $validatedParameters = $validatedParameters->with('client_secret', $this->createClientSecret());
273
            $validatedParameters = $validatedParameters->with('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : time() + $this->secretLifetime));
274
        } elseif ('private_key_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
275
            if (!($commandParameters->has('jwks') xor $commandParameters->has('jwks_uri'))) {
276
                throw new \InvalidArgumentException('The parameter "jwks" or "jwks_uri" must be set.');
277
            }
278
            if ($commandParameters->has('jwks')) {
279
                $jwks = JWKSet::createFromKeyData($commandParameters->get('jwks'));
280
                if (!$jwks instanceof JWKSet) {
281
                    throw new \InvalidArgumentException('The parameter "jwks" must be a valid JWKSet object.');
282
                }
283
                $validatedParameters = $validatedParameters->with('jwks', $commandParameters->get('jwks'));
284
            } else {
285
                $jwks = $this->jkuFactory->loadFromUrl($commandParameters->get('jwks_uri'));
286
                if (empty($jwks)) {
287
                    throw new \InvalidArgumentException('The parameter "jwks_uri" must be a valid uri to a JWK Set and at least one key.');
288
                }
289
                $validatedParameters = $validatedParameters->with('jwks_uri', $commandParameters->get('jwks_uri'));
290
            }
291
        } else {
292
            throw new \InvalidArgumentException('Unsupported token endpoint authentication method.');
293
        }
294
295
        return $validatedParameters;
296
    }
297
298
    /**
299
     * @return string
300
     */
301
    private function createClientSecret(): string
302
    {
303
        return bin2hex(random_bytes(128));
304
    }
305
306
    /**
307
     * @param Client $client
308
     * @param array  $claims
309
     *
310
     * @return JWKSet
311
     */
312
    private function retrieveIssuerKeySet(Client $client, array $claims): JWKSet
313
    {
314
        if ($claims['sub'] === $claims['iss']) { // The client is the issuer
315
            return $this->getClientKeySet($client);
316
        }
317
318
        if (null === $this->trustedIssuerRepository || null === $trustedIssuer = $this->trustedIssuerRepository->find($claims['iss'])) {
319
            throw new \InvalidArgumentException('Unable to retrieve the key set of the issuer.');
320
        }
321
322
        return $trustedIssuer->getJWKSet();
323
    }
324
325
    /**
326
     * @param Client $client
327
     * @return JWKSet
328
     */
329
    private function getClientKeySet(Client $client): JWKSet
330
    {
331
        switch (true) {
332
            case $client->has('jwks'):
333
                return JWKSet::createFromJson($client->get('jwks'));
334
            case $client->has('client_secret') && in_array($client->getTokenEndpointAuthenticationMethod(), $this->getSupportedMethods()):
335
                $jwk = JWK::create([
336
                    'kty' => 'oct',
337
                    'use' => 'sig',
338
                    'k'   => Base64Url::encode($client->get('client_secret'))
339
                ]);
340
341
                return JWKSet::createFromKeys([$jwk]);
342
            case $client->has('jwks_uri') && null !== $this->jkuFactory:
343
                return $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
344
            default:
345
                throw new \InvalidArgumentException('The client has no key or key set.');
346
        }
347
    }
348
}
349