Failed Conditions
Push — ng ( f9780e...ccd5de )
by Florent
11:07
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\JWKSet;
18
use Jose\Component\Encryption\JWELoader;
19
use Jose\Component\Signature\JWSLoader;
20
use OAuth2Framework\Component\Server\Core\Client\Client;
21
use OAuth2Framework\Component\Server\Core\Client\ClientId;
22
use OAuth2Framework\Component\Server\Core\DataBag\DataBag;
23
use OAuth2Framework\Component\Server\Core\Response\OAuth2Exception;
24
use Psr\Http\Message\ServerRequestInterface;
25
26
final class ClientAssertionJwt implements AuthenticationMethod
27
{
28
    /**
29
     * @var JWSLoader
30
     */
31
    private $jwsLoader;
32
33
    /**
34
     * @var null|JWELoader
35
     */
36
    private $jweLoader = null;
37
38
    /**
39
     * @var null|JWKSet
40
     */
41
    private $keyEncryptionKeySet = null;
42
43
    /**
44
     * @var bool
45
     */
46
    private $encryptionRequired = false;
47
48
    /**
49
     * @var int
50
     */
51
    private $secretLifetime;
52
53
    /**
54
     * @var ClaimCheckerManager
55
     */
56
    private $claimCheckerManager;
57
58
    /**
59
     * ClientAssertionJwt constructor.
60
     *
61
     * @param JWSLoader           $jwsLoader
62
     * @param ClaimCheckerManager $claimCheckerManager
63
     * @param int                 $secretLifetime
64
     */
65
    public function __construct(JWSLoader $jwsLoader, ClaimCheckerManager $claimCheckerManager, int $secretLifetime = 0)
66
    {
67
        if ($secretLifetime < 0) {
68
            throw new \InvalidArgumentException('The secret lifetime must be at least 0 (= unlimited).');
69
        }
70
        $this->jwsLoader = $jwsLoader;
71
        $this->claimCheckerManager = $claimCheckerManager;
72
        $this->secretLifetime = $secretLifetime;
73
    }
74
75
    /**
76
     * @param JWELoader $jweLoader
77
     * @param JWKSet    $keyEncryptionKeySet
78
     * @param bool      $encryptionRequired
79
     */
80
    public function enableEncryptedAssertions(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $encryptionRequired)
81
    {
82
        $this->jweLoader = $jweLoader;
83
        $this->encryptionRequired = $encryptionRequired;
84
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
85
    }
86
87
    /**
88
     * @return string[]
89
     */
90
    public function getSupportedSignatureAlgorithms(): array
91
    {
92
        return $this->jwsLoader->getSignatureAlgorithmManager()->list();
0 ignored issues
show
Bug introduced by
The method getSignatureAlgorithmManager() does not seem to exist on object<Jose\Component\Signature\JWSLoader>.

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...
93
    }
94
95
    /**
96
     * @return string[]
97
     */
98
    public function getSupportedContentEncryptionAlgorithms(): array
99
    {
100
        return null === $this->jweLoader ? [] : $this->jweLoader->getContentEncryptionAlgorithmManager()->list();
101
    }
102
103
    /**
104
     * @return string[]
105
     */
106
    public function getSupportedKeyEncryptionAlgorithms(): array
107
    {
108
        return null === $this->jweLoader ? [] : $this->jweLoader->getKeyEncryptionAlgorithmManager()->list();
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114
    public function getSchemesParameters(): array
115
    {
116
        return [];
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ? ClientId
123
    {
124
        $parameters = $request->getParsedBody() ?? [];
125
        if (!array_key_exists('client_assertion_type', $parameters)) {
126
            return null;
127
        }
128
        $clientAssertionType = $parameters['client_assertion_type'];
129
130
        //We verify the client assertion type in the request
131
        if ('urn:ietf:params:oauth:client-assertion-type:jwt-bearer' !== $clientAssertionType) {
132
            return null;
133
        }
134
135
        try {
136
            if (!array_key_exists('client_assertion', $parameters)) {
137
                throw new \InvalidArgumentException('Parameter "client_assertion" is missing.');
138
            }
139
            $client_assertion = $parameters['client_assertion'];
140
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
141
            $jws = $this->jwsSerializer->unserialize($client_assertion);
0 ignored issues
show
Bug introduced by
The property jwsSerializer does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
142
            if (1 !== $jws->countSignatures()) {
143
                throw new \InvalidArgumentException('The assertion must have only one signature.');
144
            }
145
            $claims = json_decode($jws->getPayload(), true);
146
            $claims = $this->claimCheckerManager->check($claims);
147
148
            $diff = array_diff(['iss', 'sub', 'aud', 'jti', 'exp'], array_keys($claims));
149
            if (!empty($diff)) {
150
                throw new \InvalidArgumentException(sprintf('The following claim(s) is/are mandatory: "%s".', implode(', ', array_values($diff))));
151
            }
152
            if ($claims['sub'] !== $claims['iss']) {
153
                throw new \InvalidArgumentException('The claims "sub" and "iss" must contain the client public ID.');
154
            }
155
        } catch (\Exception $e) {
156
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, $e->getMessage(), [], $e);
157
        }
158
159
        $clientCredentials = $jws;
160
161
        return ClientId::create($claims['sub']);
162
    }
163
164
    /**
165
     * @param string $assertion
166
     *
167
     * @return string
168
     *
169
     * @throws OAuth2Exception
170
     */
171
    private function tryToDecryptClientAssertion(string $assertion): string
172
    {
173
        if (null === $this->jweLoader) {
174
            return $assertion;
175
        }
176
177
        try {
178
            $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...
179
            if (1 !== $jwe->countRecipients()) {
180
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
181
            }
182
183
            return $jwe->getPayload();
184
        } catch (\Exception $e) {
185
            if (true === $this->encryptionRequired) {
186
                throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, $e->getMessage(), [], $e);
187
            }
188
189
            return $assertion;
190
        }
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
197
    {
198
        try {
199
            //Get the JWKSet depending on the client configuration and parameters
200
            $jwkSet = $client->getPublicKeySet();
201
            Assertion::isInstanceOf($jwkSet, JWKSet::class);
202
            $this->jwsVerifier->verifyWithKeySet($clientCredentials, $jwkSet);
0 ignored issues
show
Bug introduced by
The property jwsVerifier does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
203
        } catch (\Exception $e) {
204
            return false;
205
        }
206
207
        return true;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213
    public function getSupportedMethods(): array
214
    {
215
        return ['client_secret_jwt', 'private_key_jwt'];
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
222
    {
223
        if ('client_secret_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
224
            $validatedParameters = $validatedParameters->with('client_secret', $this->createClientSecret());
225
            $validatedParameters = $validatedParameters->with('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : time() + $this->secretLifetime));
226
        } elseif ('private_key_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
227
            if (!($commandParameters->has('jwks') xor $commandParameters->has('jwks_uri'))) {
228
                throw new \InvalidArgumentException('The parameter "jwks" or "jwks_uri" must be set.');
229
            }
230
            if ($commandParameters->has('jwks')) {
231
                $jwks = JWKSet::createFromKeyData($commandParameters->get('jwks'));
232
                if (!$jwks instanceof JWKSet) {
233
                    throw new \InvalidArgumentException('The parameter "jwks" must be a valid JWKSet object.');
234
                }
235
                $validatedParameters = $validatedParameters->with('jwks', $commandParameters->get('jwks'));
236
            }/* else { FIXME
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% 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...
237
                $jwks = JWKFactory::createFromJKU($commandParameters->get('jwks_uri'));
238
                Assertion::isInstanceOf($jwks, JWKSet::class, 'The parameter "jwks_uri" must be a valid uri that provide a valid JWKSet.');
239
                $validatedParameters = $validatedParameters->with('jwks_uri', $commandParameters->get('jwks_uri'));
240
            }*/
241
        } else {
242
            throw new \InvalidArgumentException('Unsupported token endpoint authentication method.');
243
        }
244
245
        return $validatedParameters;
246
    }
247
248
    /**
249
     * @return string
250
     */
251
    private function createClientSecret(): string
252
    {
253
        return bin2hex(random_bytes(128));
254
    }
255
}
256