Failed Conditions
Push — ng ( 03dae5...ac104a )
by Florent
04:27
created

ClientAssertionJwt::getSupportedMethods()   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\Server\TokenEndpoint\AuthenticationMethod;
15
16
use Jose\Component\Checker\ClaimCheckerManager;
17
use Jose\Component\Core\Converter\StandardConverter;
18
use Jose\Component\Core\JWKSet;
19
use Jose\Component\Encryption\JWELoader;
20
use Jose\Component\KeyManagement\JKUFactory;
21
use Jose\Component\Signature\JWS;
22
use Jose\Component\Signature\JWSVerifier;
23
use Jose\Component\Signature\Serializer\CompactSerializer;
24
use OAuth2Framework\Component\Server\Core\Client\Client;
25
use OAuth2Framework\Component\Server\Core\Client\ClientId;
26
use OAuth2Framework\Component\Server\Core\DataBag\DataBag;
27
use OAuth2Framework\Component\Server\Core\Response\OAuth2Exception;
28
use Psr\Http\Message\ServerRequestInterface;
29
30
final class ClientAssertionJwt implements AuthenticationMethod
31
{
32
    /**
33
     * @var TrustedIssuerManager
34
     */
35
    private $trustedIssuerManager;
36
37
    /**
38
     * @var JWSVerifier
39
     */
40
    private $jwsVerifier;
41
42
    /**
43
     * @var JKUFactory
44
     */
45
    private $jkuFactory;
46
47
    /**
48
     * @var null|JWELoader
49
     */
50
    private $jweLoader = null;
51
52
    /**
53
     * @var null|JWKSet
54
     */
55
    private $keyEncryptionKeySet = null;
56
57
    /**
58
     * @var bool
59
     */
60
    private $encryptionRequired = false;
61
62
    /**
63
     * @var int
64
     */
65
    private $secretLifetime;
66
67
    /**
68
     * @var ClaimCheckerManager
69
     */
70
    private $claimCheckerManager;
71
72
    /**
73
     * ClientAssertionJwt constructor.
74
     *
75
     * @param TrustedIssuerManager $trustedIssuerManager
76
     * @param JKUFactory           $jkuFactory
77
     * @param JWSVerifier          $jwsVerifier
78
     * @param ClaimCheckerManager  $claimCheckerManager
79
     * @param int                  $secretLifetime
80
     */
81
    public function __construct(TrustedIssuerManager $trustedIssuerManager, JKUFactory $jkuFactory, JWSVerifier $jwsVerifier, ClaimCheckerManager $claimCheckerManager, int $secretLifetime = 0)
82
    {
83
        if ($secretLifetime < 0) {
84
            throw new \InvalidArgumentException('The secret lifetime must be at least 0 (= unlimited).');
85
        }
86
        $this->trustedIssuerManager = $trustedIssuerManager;
87
        $this->jkuFactory = $jkuFactory;
88
        $this->jwsVerifier = $jwsVerifier;
89
        $this->claimCheckerManager = $claimCheckerManager;
90
        $this->secretLifetime = $secretLifetime;
91
    }
92
93
    /**
94
     * @param JWELoader $jweLoader
95
     * @param JWKSet    $keyEncryptionKeySet
96
     * @param bool      $encryptionRequired
97
     */
98
    public function enableEncryptedAssertions(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $encryptionRequired)
99
    {
100
        $this->jweLoader = $jweLoader;
101
        $this->encryptionRequired = $encryptionRequired;
102
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
103
    }
104
105
    /**
106
     * @return string[]
107
     */
108
    public function getSupportedSignatureAlgorithms(): array
109
    {
110
        return $this->jwsVerifier->getSignatureAlgorithmManager()->list();
111
    }
112
113
    /**
114
     * @return string[]
115
     */
116
    public function getSupportedContentEncryptionAlgorithms(): array
117
    {
118
        return null === $this->jweLoader ? [] : $this->jweLoader->getContentEncryptionAlgorithmManager()->list();
119
    }
120
121
    /**
122
     * @return string[]
123
     */
124
    public function getSupportedKeyEncryptionAlgorithms(): array
125
    {
126
        return null === $this->jweLoader ? [] : $this->jweLoader->getKeyEncryptionAlgorithmManager()->list();
127
    }
128
129
    /**
130
     * {@inheritdoc}
131
     */
132
    public function getSchemesParameters(): array
133
    {
134
        return [];
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ? ClientId
141
    {
142
        $parameters = $request->getParsedBody() ?? [];
143
        if (!array_key_exists('client_assertion_type', $parameters)) {
144
            return null;
145
        }
146
        $clientAssertionType = $parameters['client_assertion_type'];
147
148
        if ('urn:ietf:params:oauth:client-assertion-type:jwt-bearer' !== $clientAssertionType) {
149
            return null;
150
        }
151
152
        try {
153
            if (!array_key_exists('client_assertion', $parameters)) {
154
                throw new \InvalidArgumentException('Parameter "client_assertion" is missing.');
155
            }
156
            $client_assertion = $parameters['client_assertion'];
157
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
158
            $serializer = new CompactSerializer(new StandardConverter());
159
            $jws = $serializer->unserialize($client_assertion);
160
            if (1 !== $jws->countSignatures()) {
161
                throw new \InvalidArgumentException('The assertion must have only one signature.');
162
            }
163
            $claims = json_decode($jws->getPayload(), true);
164
165
            // Other claims can be considered as mandatory
166
            $diff = array_diff(['iss', 'sub', 'aud', 'exp'], array_keys($claims));
167
            if (!empty($diff)) {
168
                throw new \InvalidArgumentException(sprintf('The following claim(s) is/are mandatory: "%s".', implode(', ', array_values($diff))));
169
            }
170
171
            $clientCredentials = $jws;
172
173
            return ClientId::create($claims['sub']);
174
        } catch (\Exception $e) {
175
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, $e->getMessage(), [], $e);
176
        }
177
178
    }
179
180
    /**
181
     * @param string $assertion
182
     *
183
     * @return string
184
     *
185
     * @throws OAuth2Exception
186
     */
187
    private function tryToDecryptClientAssertion(string $assertion): string
188
    {
189
        if (null === $this->jweLoader) {
190
            return $assertion;
191
        }
192
193
        try {
194
            $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...
195
            if (1 !== $jwe->countRecipients()) {
196
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
197
            }
198
199
            return $jwe->getPayload();
200
        } catch (\Exception $e) {
201
            if (true === $this->encryptionRequired) {
202
                throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, $e->getMessage(), [], $e);
203
            }
204
205
            return $assertion;
206
        }
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
213
    {
214
        try {
215
            /** @var JWS $jws */
216
            $jws = $clientCredentials;
217
218
            //Retrieve the JWKSet from the client configuration or the trusted issuer (see iss claim)
219
            $this->jwsVerifier->verifyWithKeySet($jws);
0 ignored issues
show
Bug introduced by
The call to verifyWithKeySet() misses some required arguments starting with $jwkset.
Loading history...
220
            // The claim checker manager should return only checked claims
221
            $claims = $this->claimCheckerManager->check($claims);
0 ignored issues
show
Bug introduced by
The variable $claims seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
222
223
            //Trusted issuers should be supported
224
            if ($claims['sub'] !== $claims['iss']) {
225
                throw new \InvalidArgumentException('The claims "sub" and "iss" must contain the client public ID.');
226
            }
227
            //Get the JWKSet depending on the client configuration and parameters
228
            $jwkSet = $client->getPublicKeySet();
229
            Assertion::isInstanceOf($jwkSet, JWKSet::class);
230
            $this->jwsVerifier->verifyWithKeySet($clientCredentials, $jwkSet);
0 ignored issues
show
Bug introduced by
The call to verifyWithKeySet() misses a required argument $signature.

This check looks for function calls that miss required arguments.

Loading history...
231
        } catch (\Exception $e) {
232
            return false;
233
        }
234
235
        return true;
236
    }
237
238
    /**
239
     * {@inheritdoc}
240
     */
241
    public function getSupportedMethods(): array
242
    {
243
        return ['client_secret_jwt', 'private_key_jwt'];
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
250
    {
251
        if ('client_secret_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
252
            $validatedParameters = $validatedParameters->with('client_secret', $this->createClientSecret());
253
            $validatedParameters = $validatedParameters->with('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : time() + $this->secretLifetime));
254
        } elseif ('private_key_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
255
            if (!($commandParameters->has('jwks') xor $commandParameters->has('jwks_uri'))) {
256
                throw new \InvalidArgumentException('The parameter "jwks" or "jwks_uri" must be set.');
257
            }
258
            if ($commandParameters->has('jwks')) {
259
                $jwks = JWKSet::createFromKeyData($commandParameters->get('jwks'));
260
                if (!$jwks instanceof JWKSet) {
261
                    throw new \InvalidArgumentException('The parameter "jwks" must be a valid JWKSet object.');
262
                }
263
                $validatedParameters = $validatedParameters->with('jwks', $commandParameters->get('jwks'));
264
            } else {
265
                $jwks = $this->jkuFactory->loadFromUrl($commandParameters->get('jwks_uri'));
266
                if (empty($jwks)) {
267
                    throw new \InvalidArgumentException('The parameter "jwks_uri" must be a valid uri to a JWK Set and at least one key.');
268
                }
269
                $validatedParameters = $validatedParameters->with('jwks_uri', $commandParameters->get('jwks_uri'));
270
            }
271
        } else {
272
            throw new \InvalidArgumentException('Unsupported token endpoint authentication method.');
273
        }
274
275
        return $validatedParameters;
276
    }
277
278
    /**
279
     * @return string
280
     */
281
    private function createClientSecret(): string
282
    {
283
        return bin2hex(random_bytes(128));
284
    }
285
}
286