Failed Conditions
Push — ng ( 75309d...bc6f2d )
by Florent
08:22
created

ClientAssertionJwt   B

Complexity

Total Complexity 34

Size/Duplication

Total Lines 255
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 15

Importance

Changes 0
Metric Value
wmc 34
lcom 3
cbo 15
dl 0
loc 255
rs 8.4332
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 2
A enableEncryptedAssertions() 0 6 1
A getSupportedSignatureAlgorithms() 0 4 1
A getSupportedContentEncryptionAlgorithms() 0 4 2
A getSupportedKeyEncryptionAlgorithms() 0 4 2
A getSchemesParameters() 0 4 1
C findClientIdAndCredentials() 0 38 7
B tryToDecryptClientAssertion() 0 21 5
B isClientAuthenticated() 0 25 3
A getSupportedMethods() 0 4 1
C checkClientConfiguration() 0 28 8
A createClientSecret() 0 4 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 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\Core\Client\Client;
25
use OAuth2Framework\Component\Core\Client\ClientId;
26
use OAuth2Framework\Component\Core\DataBag\DataBag;
27
use OAuth2Framework\Component\Core\Exception\OAuth2Exception;
28
use Psr\Http\Message\ServerRequestInterface;
29
30
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();
0 ignored issues
show
Bug introduced by
The method getContentEncryptionAlgorithmManager() does not seem to exist on object<Jose\Component\Encryption\JWELoader>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
119
    }
120
121
    /**
122
     * @return string[]
123
     */
124
    public function getSupportedKeyEncryptionAlgorithms(): array
125
    {
126
        return null === $this->jweLoader ? [] : $this->jweLoader->getKeyEncryptionAlgorithmManager()->list();
0 ignored issues
show
Bug introduced by
The method getKeyEncryptionAlgorithmManager() does not seem to exist on object<Jose\Component\Encryption\JWELoader>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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
     * @param string $assertion
181
     *
182
     * @return string
183
     *
184
     * @throws OAuth2Exception
185
     */
186
    private function tryToDecryptClientAssertion(string $assertion): string
187
    {
188
        if (null === $this->jweLoader) {
189
            return $assertion;
190
        }
191
192
        try {
193
            $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...
194
            if (1 !== $jwe->countRecipients()) {
195
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
196
            }
197
198
            return $jwe->getPayload();
199
        } catch (\Exception $e) {
200
            if (true === $this->encryptionRequired) {
201
                throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, $e->getMessage(), $e);
202
            }
203
204
            return $assertion;
205
        }
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
212
    {
213
        try {
214
            /** @var JWS $jws */
215
            $jws = $clientCredentials;
216
217
            //Retrieve the JWKSet from the client configuration or the trusted issuer (see iss claim)
218
            $this->jwsVerifier->verifyWithKeySet($jws);
0 ignored issues
show
Bug introduced by
The call to verifyWithKeySet() misses some required arguments starting with $jwkset.
Loading history...
219
            // The claim checker manager should return only checked claims
220
            $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...
221
222
            //Trusted issuers should be supported
223
            if ($claims['sub'] !== $claims['iss']) {
224
                throw new \InvalidArgumentException('The claims "sub" and "iss" must contain the client public ID.');
225
            }
226
            //Get the JWKSet depending on the client configuration and parameters
227
            $jwkSet = $client->getPublicKeySet();
228
            //Assertion::isInstanceOf($jwkSet, JWKSet::class);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

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