Failed Conditions
Push — ng ( 68a719...06acb0 )
by Florent
23:02
created

ClientAssertionJwt::isClientAuthenticated()   A

Complexity

Conditions 2
Paths 4

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
nc 4
nop 3
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\JWEDecrypter;
19
use Jose\Component\Encryption\Serializer\CompactSerializer as JweCompactSerializer;
20
use Jose\Component\Signature\JWSVerifier;
21
use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer;
22
use OAuth2Framework\Component\Server\Core\Client\Client;
23
use OAuth2Framework\Component\Server\Core\Client\ClientId;
24
use OAuth2Framework\Component\Server\Core\DataBag\DataBag;
25
use OAuth2Framework\Component\Server\Core\Response\OAuth2Exception;
26
use Psr\Http\Message\ServerRequestInterface;
27
28
final class ClientAssertionJwt implements AuthenticationMethod
29
{
30
    /**
31
     * @var JwsCompactSerializer
32
     */
33
    private $jwsSerializer;
34
35
    /**
36
     * @var JWSVerifier
37
     */
38
    private $jwsVerifier;
39
40
    /**
41
     * @var JweCompactSerializer
42
     */
43
    private $jweSerializer;
44
45
    /**
46
     * @var JWEDecrypter
47
     */
48
    private $jweDecrypter;
49
50
    /**
51
     * @var bool
52
     */
53
    private $encryptionRequired = false;
54
55
    /**
56
     * @var JWKSet|null
57
     */
58
    private $keyEncryptionKeySet = null;
59
60
    /**
61
     * @var int
62
     */
63
    private $secretLifetime;
64
65
    /**
66
     * @var ClaimCheckerManager
67
     */
68
    private $claimCheckerManager;
69
70
    /**
71
     * ClientAssertionJwt constructor.
72
     *
73
     * @param JwsCompactSerializer $jwsSerializer
74
     * @param JWSVerifier          $jwsVerifier
75
     * @param ClaimCheckerManager  $claimCheckerManager
76
     * @param int                  $secretLifetime
77
     */
78
    public function __construct(JwsCompactSerializer $jwsSerializer, JWSVerifier $jwsVerifier, ClaimCheckerManager $claimCheckerManager, int $secretLifetime = 0)
79
    {
80
        if ($secretLifetime < 0) {
81
            throw new \InvalidArgumentException('The secret lifetime must be at least 0 (= unlimited).');
82
        }
83
        $this->jwsSerializer = $jwsSerializer;
84
        $this->jwsVerifier = $jwsVerifier;
85
        $this->claimCheckerManager = $claimCheckerManager;
86
        $this->secretLifetime = $secretLifetime;
87
    }
88
89
    /**
90
     * @param JweCompactSerializer $jweSerializer
91
     * @param JWEDecrypter         $jweDecrypter
92
     * @param JWKSet               $keyEncryptionKeySet
93
     * @param bool                 $encryptionRequired
94
     */
95
    public function enableEncryptedAssertions(JweCompactSerializer $jweSerializer, JWEDecrypter $jweDecrypter, JWKSet $keyEncryptionKeySet, bool $encryptionRequired)
96
    {
97
        $this->jweSerializer = $jweSerializer;
98
        $this->jweDecrypter = $jweDecrypter;
99
        $this->encryptionRequired = $encryptionRequired;
100
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
101
    }
102
103
    /**
104
     * @return string[]
105
     */
106
    public function getSupportedSignatureAlgorithms(): array
107
    {
108
        return $this->jwsVerifier->getSignatureAlgorithmManager()->list();
109
    }
110
111
    /**
112
     * @return string[]
113
     */
114
    public function getSupportedContentEncryptionAlgorithms(): array
115
    {
116
        return null === $this->jweDecrypter ? [] : $this->jweDecrypter->getContentEncryptionAlgorithmManager()->list();
117
    }
118
119
    /**
120
     * @return string[]
121
     */
122
    public function getSupportedKeyEncryptionAlgorithms(): array
123
    {
124
        return null === $this->jweDecrypter ? [] : $this->jweDecrypter->getKeyEncryptionAlgorithmManager()->list();
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130
    public function getSchemesParameters(): array
131
    {
132
        return [];
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ? ClientId
139
    {
140
        $parameters = $request->getParsedBody() ?? [];
141
        if (!array_key_exists('client_assertion_type', $parameters)) {
142
            return null;
143
        }
144
        $clientAssertionType = $parameters['client_assertion_type'];
145
146
        //We verify the client assertion type in the request
147
        if ('urn:ietf:params:oauth:client-assertion-type:jwt-bearer' !== $clientAssertionType) {
148
            return null;
149
        }
150
151
        try {
152
            if (!array_key_exists('client_assertion', $parameters)) {
153
                throw new \InvalidArgumentException('Parameter "client_assertion" is missing.');
154
            }
155
            $client_assertion = $parameters['client_assertion'];
156
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
157
            $jws = $this->jwsSerializer->unserialize($client_assertion);
158
            if (1 !== $jws->countSignatures()) {
159
                throw new \InvalidArgumentException('The assertion must have only one signature.');
160
            }
161
            $claims = json_decode($jws->getPayload(), true);
162
            $claims = $this->claimCheckerManager->check($claims);
163
164
            $diff = array_diff(['iss', 'sub', 'aud', 'jti', 'exp'], array_keys($claims));
165
            if (!empty($diff)) {
166
                throw new \InvalidArgumentException(sprintf('The following claim(s) is/are mandatory: "%s".', implode(', ', array_values($diff))));
167
            }
168
            if ($claims['sub'] !== $claims['iss']) {
169
                throw new \InvalidArgumentException('The claims "sub" and "iss" must contain the client public ID.');
170
            }
171
        } catch (\Exception $e) {
172
            throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, $e->getMessage());
173
        }
174
175
        $clientCredentials = $jws;
176
177
        return ClientId::create($claims['sub']);
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->jweDecrypter) {
190
            return $assertion;
191
        }
192
193
        try {
194
            $jwe = $this->jweSerializer->unserialize($assertion);
195
            if (1 !== $jwe->countRecipients()) {
196
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
197
            }
198
            if (true === $this->jweDecrypter->decryptUsingKeySet($jwe, $this->keyEncryptionKeySet, 0)) {
0 ignored issues
show
Bug introduced by
It seems like $this->keyEncryptionKeySet can be null; however, decryptUsingKeySet() 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...
199
                return $jwe->getPayload();
200
            }
201
202
            throw new \InvalidArgumentException('Unable to decrypt the client assertion.');
203
        } catch (\Exception $e) {
204
            if (true === $this->encryptionRequired) {
205
                throw new OAuth2Exception(400, OAuth2Exception::ERROR_INVALID_REQUEST, $e->getMessage(), [], $e);
206
            }
207
208
            return $assertion;
209
        }
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
216
    {
217
        try {
218
            //Get the JWKSet depending on the client configuration and parameters
219
            $jwkSet = $client->getPublicKeySet();
220
            Assertion::isInstanceOf($jwkSet, JWKSet::class);
221
            $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...
222
        } catch (\Exception $e) {
223
            return false;
224
        }
225
226
        return true;
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     */
232
    public function getSupportedMethods(): array
233
    {
234
        return ['client_secret_jwt', 'private_key_jwt'];
235
    }
236
237
    /**
238
     * {@inheritdoc}
239
     */
240
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
241
    {
242
        if ('client_secret_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
243
            $validatedParameters = $validatedParameters->with('client_secret', $this->createClientSecret());
244
            $validatedParameters = $validatedParameters->with('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : time() + $this->secretLifetime));
245
        } elseif ('private_key_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
246
            if (!($commandParameters->has('jwks') xor $commandParameters->has('jwks_uri'))) {
247
                throw new \InvalidArgumentException('The parameter "jwks" or "jwks_uri" must be set.');
248
            }
249
            if ($commandParameters->has('jwks')) {
250
                $jwks = JWKSet::createFromKeyData($commandParameters->get('jwks'));
251
                if (!$jwks instanceof JWKSet) {
252
                    throw new \InvalidArgumentException('The parameter "jwks" must be a valid JWKSet object.');
253
                }
254
                $validatedParameters = $validatedParameters->with('jwks', $commandParameters->get('jwks'));
255
            }/* 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...
256
                $jwks = JWKFactory::createFromJKU($commandParameters->get('jwks_uri'));
257
                Assertion::isInstanceOf($jwks, JWKSet::class, 'The parameter "jwks_uri" must be a valid uri that provide a valid JWKSet.');
258
                $validatedParameters = $validatedParameters->with('jwks_uri', $commandParameters->get('jwks_uri'));
259
            }*/
260
        } else {
261
            throw new \InvalidArgumentException('Unsupported token endpoint authentication method.');
262
        }
263
264
        return $validatedParameters;
265
    }
266
267
    /**
268
     * @return string
269
     */
270
    private function createClientSecret(): string
271
    {
272
        return bin2hex(random_bytes(128));
273
    }
274
}
275